From fec70fcf476c0bdc115ea2f921b903d270b60c0b Mon Sep 17 00:00:00 2001 From: Anton Gildebrand Date: Mon, 5 Jan 2015 17:13:50 +0100 Subject: [PATCH 001/249] parseMacroSyntax accepts other characters than a-z I had problems with parseMacroSyntax when `syntax` included Swedish characters. Now it works with other characters than a-z as well. --- .../src/common/services/macro.service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js index e06877b28d..bcbe2ca63a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js @@ -12,8 +12,8 @@ function macroService() { /** parses the special macro syntax like and returns an object with the macro alias and it's parameters */ parseMacroSyntax: function (syntax) { - - var expression = /(<\?UMBRACO_MACRO macroAlias=["']([\w\.]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; + + var expression = /(<\?UMBRACO_MACRO macroAlias=["']([\s\S.]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; var match = expression.exec(syntax); if (!match || match.length < 3) { return null; @@ -157,4 +157,4 @@ function macroService() { } -angular.module('umbraco.services').factory('macroService', macroService); \ No newline at end of file +angular.module('umbraco.services').factory('macroService', macroService); From 9c8e6ab86491021bec7b64aa78e446b0ae7f411a Mon Sep 17 00:00:00 2001 From: mattbrailsford Date: Mon, 26 Jan 2015 12:35:01 +0000 Subject: [PATCH 002/249] Add "config" support to the macro grid property editor to allow predefining the macro to use --- .../views/propertyeditors/grid/editors/macro.controller.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js index e9eef022e7..ac5399cb75 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.controller.js @@ -8,7 +8,10 @@ angular.module("umbraco") dialogService.macroPicker({ dialogData: { richTextEditor: true, - macroData: $scope.control.value + macroData: $scope.control.value || { + macroAlias: $scope.control.editor.config && $scope.control.editor.config.macroAlias + ? $scope.control.editor.config.macroAlias : "" + } }, callback: function (data) { $scope.control.value = { From 6f2fa2ed4d49129557a12f6f14ad880e3a4d886c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 26 Feb 2015 12:38:03 +0100 Subject: [PATCH 003/249] Makes sure that the version comment gets cleared when there is no comment. --- build/Build.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.proj b/build/Build.proj index 20c36e18aa..bd037b3569 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -327,7 +327,7 @@ ReplacementText="$(BUILD_RELEASE)"/> Date: Fri, 27 Feb 2015 01:04:53 +0100 Subject: [PATCH 004/249] removes duplicate attribute --- .../src/views/directives/umb-content-name.html | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html b/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html index 85929d1e1b..4dcabd8945 100644 --- a/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html +++ b/src/Umbraco.Web.UI.Client/src/views/directives/umb-content-name.html @@ -6,7 +6,6 @@ name="name" localize="placeholder" class="umb-headline" - localize="placeholder" select-on-focus placeholder="{{placeholder}}" ng-model="model" From ded1def8e2e7ea1a4fd0f849cc7a3f1f97cd8242 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Feb 2015 16:50:33 +1100 Subject: [PATCH 005/249] Fixes: U4-6333 - but should fix this better (i.e. centralize the code to clean for xss in JS like we have in c#) --- .../src/views/common/legacy.controller.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js index 81a601f531..46114042b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js @@ -8,7 +8,13 @@ * */ function LegacyController($scope, $routeParams, $element) { - $scope.legacyPath = decodeURIComponent($routeParams.url); + + var url = $routeParams.url; + var toClean = "*?(){}[];:%<>/\\|&'\""; + for (var i = 0; i < toClean.length; i++) { + url = url.replace(toClean[i], ""); + } + $scope.legacyPath = decodeURIComponent(url); } angular.module("umbraco").controller('Umbraco.LegacyController', LegacyController); \ No newline at end of file From 793ba42f9e477bec2131b2715179bc621d191c2a Mon Sep 17 00:00:00 2001 From: hAmpzter Date: Fri, 27 Feb 2015 12:42:55 +0100 Subject: [PATCH 006/249] U4-6304 ContentTypeBaseRepository.MapContentTypes throws exception with SByte / Bool type with Mysql. Force it with Convert.ToBoolean() --- .../Repositories/ContentTypeBaseRepository.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 8c1b24329c..863f9ebeb7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -27,13 +27,13 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class ContentTypeBaseRepository : PetaPocoRepositoryBase where TEntity : class, IContentTypeComposition { - protected ContentTypeBaseRepository(IDatabaseUnitOfWork work) - : base(work) + protected ContentTypeBaseRepository(IDatabaseUnitOfWork work) + : base(work) { } - protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, IRepositoryCacheProvider cache) - : base(work, cache) + protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, IRepositoryCacheProvider cache) + : base(work, cache) { } @@ -694,20 +694,20 @@ AND umbracoNode.id <> @id", mediaTypeIds = mediaTypeIds.Distinct().ToArray(); var sql = @"SELECT cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId, umbracoNode.createDate as nCreateDate, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, - umbracoNode.parentID as nParentId, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId + umbracoNode.parentID as nParentId, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, + umbracoNode.uniqueID as nUniqueId FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType - INNER JOIN cmsContentType - ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType + INNER JOIN cmsContentType + ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes ON AllowedTypes.Id = cmsContentType.nodeId LEFT JOIN cmsContentType2ContentType as ParentTypes @@ -801,30 +801,30 @@ AND umbracoNode.id <> @id", contentTypeIds = contentTypeIds.Distinct().ToArray(); var sql = @"SELECT cmsDocumentType.IsDefault as dtIsDefault, cmsDocumentType.templateNodeId as dtTemplateId, - cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, - cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, + cmsContentType.pk as ctPk, cmsContentType.alias as ctAlias, cmsContentType.allowAtRoot as ctAllowAtRoot, cmsContentType.description as ctDesc, + cmsContentType.icon as ctIcon, cmsContentType.isContainer as ctIsContainer, cmsContentType.nodeId as ctId, cmsContentType.thumbnail as ctThumb, AllowedTypes.allowedId as ctaAllowedId, AllowedTypes.SortOrder as ctaSortOrder, AllowedTypes.alias as ctaAlias, ParentTypes.parentContentTypeId as chtParentId, umbracoNode.createDate as nCreateDate, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("level") + @" as nLevel, umbracoNode.nodeObjectType as nObjectType, umbracoNode.nodeUser as nUser, - umbracoNode.parentID as nParentId, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, - umbracoNode.uniqueID as nUniqueId, - Template.alias as tAlias, Template.nodeId as tId,Template.text as tText + umbracoNode.parentID as nParentId, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("path") + @" as nPath, umbracoNode.sortOrder as nSortOrder, umbracoNode." + SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName("text") + @" as nName, umbracoNode.trashed as nTrashed, + umbracoNode.uniqueID as nUniqueId, + Template.alias as tAlias, Template.nodeId as tId,Template.text as tText FROM cmsContentType INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id LEFT JOIN cmsDocumentType ON cmsDocumentType.contentTypeNodeId = cmsContentType.nodeId LEFT JOIN ( - SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder - FROM cmsContentTypeAllowedContentType - INNER JOIN cmsContentType - ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId + SELECT cmsContentTypeAllowedContentType.Id, cmsContentTypeAllowedContentType.AllowedId, cmsContentType.alias, cmsContentTypeAllowedContentType.SortOrder + FROM cmsContentTypeAllowedContentType + INNER JOIN cmsContentType + ON cmsContentTypeAllowedContentType.AllowedId = cmsContentType.nodeId ) AllowedTypes ON AllowedTypes.Id = cmsContentType.nodeId LEFT JOIN ( - SELECT * FROM cmsTemplate - INNER JOIN umbracoNode - ON cmsTemplate.nodeId = umbracoNode.id + SELECT * FROM cmsTemplate + INNER JOIN umbracoNode + ON cmsTemplate.nodeId = umbracoNode.id ) as Template ON Template.nodeId = cmsDocumentType.templateNodeId LEFT JOIN cmsContentType2ContentType as ParentTypes @@ -872,8 +872,8 @@ AND umbracoNode.id <> @id", //get the unique list of associated templates var defaultTemplates = result .Where(x => x.ctId == currentCtId) - //use a tuple so that distinct checks both values - .Select(x => new Tuple(x.dtIsDefault, x.dtTemplateId)) + //use a tuple so that distinct checks both values (in some rare cases the dtIsDefault will not compute as bool?, so we force it with Convert.ToBoolean) + .Select(x => new Tuple(Convert.ToBoolean(x.dtIsDefault), x.dtTemplateId)) .Where(x => x.Item1.HasValue && x.Item2.HasValue) .Distinct() .OrderByDescending(x => x.Item1.Value) From bb04f83a49dcb9e421fb826bc938dbdb13a8b57e Mon Sep 17 00:00:00 2001 From: Jeavon Leopold Date: Fri, 27 Feb 2015 12:15:28 +0000 Subject: [PATCH 007/249] Fixes U4-6336 GetCropUrl - preferFocalPoint is ignored if focal point is in the center --- src/Umbraco.Web/ImageCropperBaseExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs index 12ea7b5e04..14ca6a68ba 100644 --- a/src/Umbraco.Web/ImageCropperBaseExtensions.cs +++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs @@ -88,7 +88,7 @@ namespace Umbraco.Web cropUrl.Append("?center=" + cropDataSet.FocalPoint.Top.ToString(CultureInfo.InvariantCulture) + "," + cropDataSet.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); cropUrl.Append("&mode=crop"); } - else if (crop != null && crop.Coordinates != null) + else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) { cropUrl.Append("?crop="); cropUrl.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(","); From 0295aaba3b7f5fa9ff67dc07f3dd3d91439ccb93 Mon Sep 17 00:00:00 2001 From: Jeavon Leopold Date: Fri, 27 Feb 2015 14:42:00 +0000 Subject: [PATCH 008/249] Added test for U4-6336 GetCropUrl - preferFocalPoint is ignored if focal point is in the center --- .../PropertyEditors/ImageCropperTest.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index c0135dff36..75846fb404 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -112,5 +112,17 @@ namespace Umbraco.Tests.PropertyEditors var urlString = mediaPath.GetCropUrl(width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); Assert.AreEqual(mediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); } + + /// + /// Test for preferFocalPoint when focal point is centered + /// + [Test] + public void GetCropUrl_PreferFocalPointCenter() + { + var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; + + var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); + Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); + } } } \ No newline at end of file From b003c027e5685511f09bb36f6af793f18625cddb Mon Sep 17 00:00:00 2001 From: Jeavon Leopold Date: Fri, 27 Feb 2015 14:49:36 +0000 Subject: [PATCH 009/249] Removed unused "using" references in cropper extension methods --- src/Umbraco.Web/ImageCropperBaseExtensions.cs | 28 +++++++++---------- .../ImageCropperTemplateExtensions.cs | 17 ++++------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Web/ImageCropperBaseExtensions.cs b/src/Umbraco.Web/ImageCropperBaseExtensions.cs index 14ca6a68ba..cceac8ab31 100644 --- a/src/Umbraco.Web/ImageCropperBaseExtensions.cs +++ b/src/Umbraco.Web/ImageCropperBaseExtensions.cs @@ -1,19 +1,17 @@ -using System.Globalization; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Web.Models; - -namespace Umbraco.Web +namespace Umbraco.Web { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + + using Newtonsoft.Json; + + using Umbraco.Core; + using Umbraco.Core.Logging; + using Umbraco.Web.Models; + internal static class ImageCropperBaseExtensions { diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 8c7b06a3b6..de0f8a225e 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,16 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Models; -using Umbraco.Web.PropertyEditors; - -namespace Umbraco.Web +namespace Umbraco.Web { using System.Globalization; + using System.Text; + + using Umbraco.Core; + using Umbraco.Core.Models; + using Umbraco.Web.Models; /// /// Provides extension methods for getting ImageProcessor Url from the core Image Cropper property editor From 32bedfaa6aa12696d33be71a2c5dc10b118e08ec Mon Sep 17 00:00:00 2001 From: Jason Prothero Date: Fri, 27 Feb 2015 10:06:45 -0800 Subject: [PATCH 010/249] Changed the surrounding quotes for Grid custom settings/styles to be double quotes --- src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml | 4 ++-- src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index 5027348385..6bc730e1f8 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -65,7 +65,7 @@ if(cfg != null) foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + attrs.Add(property.Name + "=\"" + property.Value.ToString() + "\""); } JObject style = contentItem.styles; @@ -76,7 +76,7 @@ cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); } return new MvcHtmlString(string.Join(" ", attrs)); diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index a752042e90..f76028d296 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -65,7 +65,7 @@ if(cfg != null) foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + attrs.Add(property.Name + "=\"" + property.Value.ToString() + "\""); } JObject style = contentItem.styles; @@ -76,7 +76,7 @@ cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); } return new MvcHtmlString(string.Join(" ", attrs)); From c1c090faabb953b057355133e9b65eae47b9005d Mon Sep 17 00:00:00 2001 From: mikkelhm Date: Sat, 28 Feb 2015 22:13:52 +0100 Subject: [PATCH 011/249] U4-4080 Missing icons in context menu for relation types --- .../RelationTypes/TreeMenu/ActionDeleteRelationType.cs | 2 +- .../developer/RelationTypes/TreeMenu/ActionNewRelationType.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs index 30b3b97801..c11a4abac6 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs @@ -59,7 +59,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string Icon { - get { return ".sprDelete"; } // .sprDelete refers to an existing sprite + get { return "delete"; } // .sprDelete refers to an existing sprite } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs index d2477b24e3..086a5da5f6 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs @@ -59,7 +59,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string Icon { - get { return ".sprNew"; } // .sprNew refers to an existing sprite + get { return "add"; } // .sprNew refers to an existing sprite } /// From bac940d4ed06f699c362a8d3ccbb64578103970b Mon Sep 17 00:00:00 2001 From: mikkelhm Date: Sat, 28 Feb 2015 22:19:17 +0100 Subject: [PATCH 012/249] U4-4080 Missing icons in context menu for relation types, update comments aswell --- .../RelationTypes/TreeMenu/ActionDeleteRelationType.cs | 2 +- .../developer/RelationTypes/TreeMenu/ActionNewRelationType.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs index c11a4abac6..c9d7a21dfb 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionDeleteRelationType.cs @@ -59,7 +59,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string Icon { - get { return "delete"; } // .sprDelete refers to an existing sprite + get { return "delete"; } // delete refers to an existing sprite } /// diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs index 086a5da5f6..2dcbbfb077 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/TreeMenu/ActionNewRelationType.cs @@ -59,7 +59,7 @@ namespace umbraco.cms.presentation.developer.RelationTypes.TreeMenu /// public string Icon { - get { return "add"; } // .sprNew refers to an existing sprite + get { return "add"; } // add refers to an existing sprite } /// From 6dd63e8a01103de4f21646406771470037b36470 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 1 Mar 2015 13:51:51 +0100 Subject: [PATCH 013/249] U4-6341 Relate document on copy doesn't create a relation record in umbracoRelations table #U4-6341 Fixed Due in version 7.2.3 --- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e0dee043d6..d9d37cb436 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1124,6 +1124,7 @@ + From 9b254f8bf9159b665ed58bf47c1a894a8de8d44a Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 1 Mar 2015 14:09:35 +0100 Subject: [PATCH 014/249] Previous commit added a IApplicationStartupHandler which broke the count in this unit test --- src/Umbraco.Tests/Plugins/TypeFinderTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index 8611570ef3..7f39759b92 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -82,8 +82,8 @@ namespace Umbraco.Tests.Plugins var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(5, typesFound.Count()); - Assert.AreEqual(5, originalTypesFound.Count()); + Assert.AreEqual(6, typesFound.Count()); + Assert.AreEqual(6, originalTypesFound.Count()); } [Test] From 21c70462c9fc465c65b13286532b4832dd48c992 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 2 Mar 2015 16:59:05 +1100 Subject: [PATCH 015/249] publicizes cache extensions --- src/Umbraco.Core/Cache/CacheProviderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Cache/CacheProviderExtensions.cs b/src/Umbraco.Core/Cache/CacheProviderExtensions.cs index 474fa00a8b..7e5b91ea6a 100644 --- a/src/Umbraco.Core/Cache/CacheProviderExtensions.cs +++ b/src/Umbraco.Core/Cache/CacheProviderExtensions.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Cache /// /// Extensions for strongly typed access /// - internal static class CacheProviderExtensions + public static class CacheProviderExtensions { public static T GetCacheItem(this IRuntimeCacheProvider provider, string cacheKey, From 56017a340c6f2ce3d8a2cc80fde7dc2e773ee9a7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 2 Mar 2015 16:59:40 +1100 Subject: [PATCH 016/249] makes PropertyEditorResolver faster by not having to re-union every time its accessed --- src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs index 441d7fde2c..90c6e184aa 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs @@ -17,14 +17,17 @@ namespace Umbraco.Core.PropertyEditors public PropertyEditorResolver(Func> typeListProducerList) : base(typeListProducerList, ObjectLifetimeScope.Application) { + _unioned = new Lazy>(() => Values.Union(ManifestBuilder.PropertyEditors).ToList()); } + private readonly Lazy> _unioned; + /// /// Returns the property editors /// public IEnumerable PropertyEditors { - get { return Values.Union(ManifestBuilder.PropertyEditors); } + get { return _unioned.Value; } } /// From 100485f748199eee5251af6e96f40c0c58b3b846 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 2 Mar 2015 17:02:25 +1100 Subject: [PATCH 017/249] Fixes: U4-6005 Please add a public constructor to InstallApiController --- src/Umbraco.Web/Install/Controllers/InstallApiController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs index 2ab1fd97ad..1f1c14f0b6 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs +++ b/src/Umbraco.Web/Install/Controllers/InstallApiController.cs @@ -21,13 +21,13 @@ namespace Umbraco.Web.Install.Controllers [HttpInstallAuthorize] public class InstallApiController : ApiController { - protected InstallApiController() + public InstallApiController() : this(UmbracoContext.Current) { } - protected InstallApiController(UmbracoContext umbracoContext) + public InstallApiController(UmbracoContext umbracoContext) { if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); UmbracoContext = umbracoContext; From f708a24401095ca7b282bbac1df2d7b5f03738fb Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Mar 2015 17:36:53 +1100 Subject: [PATCH 018/249] fixes U4-6333, U4-6348 --- .../src/views/common/legacy.controller.js | 9 ++--- src/Umbraco.Web.UI/umbraco/endPreview.aspx | 36 +++++++++++-------- src/Umbraco.Web/Umbraco.Web.csproj | 3 -- .../umbraco/endPreview.aspx.cs | 27 -------------- 4 files changed, 27 insertions(+), 48 deletions(-) delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/endPreview.aspx.cs diff --git a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js index 46114042b5..bf6bc287df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js @@ -9,12 +9,13 @@ */ function LegacyController($scope, $routeParams, $element) { - var url = $routeParams.url; - var toClean = "*?(){}[];:%<>/\\|&'\""; + var url = decodeURIComponent($routeParams.url.toLowerCase().trimStart("javascript:")); + var toClean = "*(){}[];:<>\\|'\""; for (var i = 0; i < toClean.length; i++) { - url = url.replace(toClean[i], ""); + var reg = new RegExp("\\" + toClean[i], "g"); + url = url.replace(reg, ""); } - $scope.legacyPath = decodeURIComponent(url); + $scope.legacyPath = url; } angular.module("umbraco").controller('Umbraco.LegacyController', LegacyController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/endPreview.aspx b/src/Umbraco.Web.UI/umbraco/endPreview.aspx index 915838ca53..1759dae891 100644 --- a/src/Umbraco.Web.UI/umbraco/endPreview.aspx +++ b/src/Umbraco.Web.UI/umbraco/endPreview.aspx @@ -1,16 +1,24 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="endPreview.aspx.cs" Inherits="umbraco.presentation.endPreview" %> +<%@ Page Language="C#" AutoEventWireup="true" Inherits="System.Web.UI.Page" %> - +5 \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2d51186ce4..fd2a1561e1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -934,9 +934,6 @@ ASPXCodeBehind - - ASPXCodeBehind - ASPXCodeBehind diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/endPreview.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/endPreview.aspx.cs deleted file mode 100644 index 526e6613c5..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/endPreview.aspx.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.UI; -using System.Web.UI.WebControls; - -namespace umbraco.presentation -{ - public partial class endPreview : BasePages.UmbracoEnsuredPage - { - protected void Page_Load(object sender, EventArgs e) - { - preview.PreviewContent.ClearPreviewCookie(); - Response.Redirect(helper.Request("redir"), true); - } - - /// - /// form1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlForm form1; - } -} From 36bed40db0ca6a8cdb2a1b2c19a1305cd71a272d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Mar 2015 17:37:42 +1100 Subject: [PATCH 019/249] typo --- src/Umbraco.Web.UI/umbraco/endPreview.aspx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/endPreview.aspx b/src/Umbraco.Web.UI/umbraco/endPreview.aspx index 1759dae891..a56af97ff5 100644 --- a/src/Umbraco.Web.UI/umbraco/endPreview.aspx +++ b/src/Umbraco.Web.UI/umbraco/endPreview.aspx @@ -21,4 +21,4 @@ Response.Redirect(url.ToString(), true); } -5 \ No newline at end of file + \ No newline at end of file From 8905878a8785a71742dbce7441d445bf44bd3d24 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Mar 2015 18:33:04 +1100 Subject: [PATCH 020/249] Adds some methods to TracksChangesEntityBase to enable/disable change tracking and to reset the change tracking dictionaries, this is then used for DeepCloning so that change tracking is turned off for the cloning process. --- src/Umbraco.Core/Models/Content.cs | 6 +++- src/Umbraco.Core/Models/ContentTypeBase.cs | 7 ++++- .../Models/ContentTypeCompositionBase.cs | 6 +++- src/Umbraco.Core/Models/EntityBase/Entity.cs | 8 ++++++ .../EntityBase/TracksChangesEntityBase.cs | 28 ++++++++++++++++++- src/Umbraco.Core/Models/File.cs | 7 +++-- src/Umbraco.Core/Models/Macro.cs | 7 +++-- src/Umbraco.Core/Models/Member.cs | 6 +++- src/Umbraco.Core/Models/Membership/User.cs | 7 +++-- src/Umbraco.Core/Models/Property.cs | 6 +++- src/Umbraco.Core/Models/PropertyType.cs | 7 +++-- src/Umbraco.Core/Models/Template.cs | 7 +++-- src/Umbraco.Core/Models/UmbracoEntity.cs | 8 ++++-- 13 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index 13d6bd71c9..f4af3b842e 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -460,10 +460,14 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (Content)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually clone this since it's not settable clone._contentType = (IContentType)ContentType.DeepClone(); + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 30ac526f4a..07c2067022 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -609,13 +609,18 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (ContentTypeBase)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually wire up the event handlers for the property type collections - we've ensured // its ignored from the auto-clone process because its return values are unions, not raw and // we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842 clone._propertyTypes = (PropertyTypeCollection)_propertyTypes.DeepClone(); clone._propertyTypes.CollectionChanged += clone.PropertyTypesChanged; + //this shouldn't really be needed since we're not tracking + clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index ac358481f1..1fd1496742 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -255,11 +255,15 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (ContentTypeCompositionBase)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually assign since this is an internal field and will not be automatically mapped clone.RemovedContentTypeKeyTracker = new List(); clone._contentTypeComposition = ContentTypeComposition.Select(x => (IContentTypeComposition)x.DeepClone()).ToList(); + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/EntityBase/Entity.cs b/src/Umbraco.Core/Models/EntityBase/Entity.cs index 315e2697c0..0ecdee2b54 100644 --- a/src/Umbraco.Core/Models/EntityBase/Entity.cs +++ b/src/Umbraco.Core/Models/EntityBase/Entity.cs @@ -236,9 +236,17 @@ namespace Umbraco.Core.Models.EntityBase //Memberwise clone on Entity will work since it doesn't have any deep elements // for any sub class this will work for standard properties as well that aren't complex object's themselves. var clone = (Entity)MemberwiseClone(); + //ensure the clone has it's own dictionaries + clone.ResetChangeTrackingCollections(); + //turn off change tracking + clone.DisableChangeTracking(); //Automatically deep clone ref properties that are IDeepCloneable DeepCloneHelper.DeepCloneRefProperties(this, clone); + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); + return clone; } } diff --git a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs index 2153b8f57f..7ab1b47ebb 100644 --- a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs +++ b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs @@ -18,7 +18,9 @@ namespace Umbraco.Core.Models.EntityBase public virtual IEnumerable GetDirtyProperties() { return _propertyChangedInfo.Where(x => x.Value).Select(x => x.Key); - } + } + + private bool _changeTrackingEnabled = true; /// /// Tracks the properties that have changed @@ -41,6 +43,9 @@ namespace Umbraco.Core.Models.EntityBase /// The property info. protected virtual void OnPropertyChanged(PropertyInfo propertyInfo) { + //return if we're not tracking changes + if (_changeTrackingEnabled == false) return; + _propertyChangedInfo[propertyInfo.Name] = true; if (PropertyChanged != null) @@ -132,6 +137,22 @@ namespace Umbraco.Core.Models.EntityBase _propertyChangedInfo = new Dictionary(); } + protected void ResetChangeTrackingCollections() + { + _propertyChangedInfo = new Dictionary(); + _lastPropertyChangedInfo = new Dictionary(); + } + + protected void DisableChangeTracking() + { + _changeTrackingEnabled = false; + } + + protected void EnableChangeTracking() + { + _changeTrackingEnabled = true; + } + /// /// Used by inheritors to set the value of properties, this will detect if the property value actually changed and if it did /// it will ensure that the property has a dirty flag set. @@ -150,6 +171,9 @@ namespace Umbraco.Core.Models.EntityBase var initVal = value; var newVal = setValue(value); + //don't track changes, just set the value (above) + if (_changeTrackingEnabled == false) return false; + if (Equals(initVal, newVal) == false) { OnPropertyChanged(propertySelector); @@ -157,5 +181,7 @@ namespace Umbraco.Core.Models.EntityBase } return false; } + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 3513e4e031..de32f6179c 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -113,12 +113,15 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (File)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually assign since they are readonly properties clone._alias = Alias; clone._name = Name; - + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/Macro.cs b/src/Umbraco.Core/Models/Macro.cs index 0c86f5d1fe..fcc23ed858 100644 --- a/src/Umbraco.Core/Models/Macro.cs +++ b/src/Umbraco.Core/Models/Macro.cs @@ -399,14 +399,17 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (Macro)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); clone._addedProperties = new List(); clone._removedProperties = new List(); clone._properties = (MacroPropertyCollection)Properties.DeepClone(); //re-assign the event handler clone._properties.CollectionChanged += clone.PropertiesChanged; - + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index c4c0362acc..d416c846ef 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -617,10 +617,14 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (Member)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually clone this since it's not settable clone._contentType = (IMemberType)ContentType.DeepClone(); + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index c9b55bd937..1a4d06c8d9 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -415,15 +415,18 @@ namespace Umbraco.Core.Models.Membership public override object DeepClone() { var clone = (User)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to create new collections otherwise they'll get copied by ref clone._addedSections = new List(); clone._removedSections = new List(); clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); //re-create the event handler clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; - + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index f8696794b1..4f8fa3deb2 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -160,10 +160,14 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (Property)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually assign since this is a readonly property clone._propertyType = (PropertyType)PropertyType.DeepClone(); + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index a19b7d98c2..5bf96d2578 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -456,14 +456,17 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (PropertyType)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually assign the Lazy value as it will not be automatically mapped if (PropertyGroupId != null) { clone._propertyGroupId = new Lazy(() => PropertyGroupId.Value); } - + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index fc10c818ef..72e546fcc3 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -193,12 +193,15 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (Template)base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //need to manually assign since they are readonly properties clone._alias = Alias; clone._name = Name; - + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 23a6bcfe5b..4975ad3bd2 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -288,7 +288,8 @@ namespace Umbraco.Core.Models public override object DeepClone() { var clone = (UmbracoEntity) base.DeepClone(); - + //turn off change tracking + clone.DisableChangeTracking(); //This ensures that any value in the dictionary that is deep cloneable is cloned too foreach (var key in clone.AdditionalData.Keys.ToArray()) { @@ -298,7 +299,10 @@ namespace Umbraco.Core.Models clone.AdditionalData[key] = deepCloneable.DeepClone(); } } - + //this shouldn't really be needed since we're not tracking + clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); return clone; } From 834b780d8ee8bab3f0003a06ac0c00e8b660fcc5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Mar 2015 19:37:13 +1100 Subject: [PATCH 021/249] Removes change tracking for User.DefaultPermissions since it is entirely not necessary (and in fact shouldn't be settable). Also ensures that the cloning of the default permissions is a new list, wasn't doing this before... which might have been the culprit for strange thread issues. --- src/Umbraco.Core/Models/Membership/IUser.cs | 3 ++- src/Umbraco.Core/Models/Membership/User.cs | 23 +++++++++------------ src/Umbraco.Tests/Models/UserTests.cs | 3 +++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 3601f22770..47eb074553 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -20,7 +20,8 @@ namespace Umbraco.Core.Models.Membership /// Gets/sets the user type for the user /// IUserType UserType { get; set; } - + + //TODO: This should be a private set /// /// The default permission set for the user /// diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 1a4d06c8d9..3e95a94d3a 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -27,7 +27,7 @@ namespace Umbraco.Core.Models.Membership if (userType == null) throw new ArgumentNullException("userType"); _userType = userType; - _defaultPermissions = _userType.Permissions; + _defaultPermissions = _userType.Permissions == null ? Enumerable.Empty() : new List(_userType.Permissions); //Groups = new List { userType }; SessionTimeout = 60; _sectionCollection = new ObservableCollection(); @@ -71,7 +71,9 @@ namespace Umbraco.Core.Models.Membership private bool _isApproved; private bool _isLockedOut; private string _language; - private IEnumerable _defaultPermissions; + + private IEnumerable _defaultPermissions; + private bool _defaultToLiveEditing; private static readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); @@ -86,7 +88,7 @@ namespace Umbraco.Core.Models.Membership private static readonly PropertyInfo IsLockedOutSelector = ExpressionHelper.GetPropertyInfo(x => x.IsLockedOut); private static readonly PropertyInfo IsApprovedSelector = ExpressionHelper.GetPropertyInfo(x => x.IsApproved); private static readonly PropertyInfo LanguageSelector = ExpressionHelper.GetPropertyInfo(x => x.Language); - private static readonly PropertyInfo DefaultPermissionsSelector = ExpressionHelper.GetPropertyInfo>(x => x.DefaultPermissions); + private static readonly PropertyInfo DefaultToLiveEditingSelector = ExpressionHelper.GetPropertyInfo(x => x.DefaultToLiveEditing); private static readonly PropertyInfo UserTypeSelector = ExpressionHelper.GetPropertyInfo(x => x.UserType); @@ -319,19 +321,13 @@ namespace Umbraco.Core.Models.Membership }, _language, LanguageSelector); } } - + + //TODO: This should be a private set [DataMember] public IEnumerable DefaultPermissions { - get { return _defaultPermissions; } - set - { - SetPropertyValueAndDetectChanges(o => - { - _defaultPermissions = value; - return _defaultPermissions; - }, _defaultPermissions, DefaultPermissionsSelector); - } + get { return _defaultPermissions;} + set { _defaultPermissions = value; } } [IgnoreDataMember] @@ -421,6 +417,7 @@ namespace Umbraco.Core.Models.Membership clone._addedSections = new List(); clone._removedSections = new List(); clone._sectionCollection = new ObservableCollection(_sectionCollection.ToList()); + clone._defaultPermissions = new List(_defaultPermissions.ToList()); //re-create the event handler clone._sectionCollection.CollectionChanged += clone.SectionCollectionChanged; //this shouldn't really be needed since we're not tracking diff --git a/src/Umbraco.Tests/Models/UserTests.cs b/src/Umbraco.Tests/Models/UserTests.cs index 7fa9742870..c686a78dc8 100644 --- a/src/Umbraco.Tests/Models/UserTests.cs +++ b/src/Umbraco.Tests/Models/UserTests.cs @@ -51,6 +51,9 @@ namespace Umbraco.Tests.Models Assert.AreEqual(clone.UserType, item.UserType); Assert.AreEqual(clone.AllowedSections.Count(), item.AllowedSections.Count()); + Assert.AreNotSame(clone.DefaultPermissions, item.DefaultPermissions); + Assert.AreEqual(clone.DefaultPermissions.Count(), item.DefaultPermissions.Count()); + //Verify normal properties with reflection var allProps = clone.GetType().GetProperties(); foreach (var propertyInfo in allProps) From 9a042fbbdf5c785356572c1a239f4c4cc63007c3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 3 Mar 2015 19:40:37 +1100 Subject: [PATCH 022/249] Removes the assignment of the DefaultPermissions property since this is auto assigned in the ctor (and should be assignable) --- src/Umbraco.Core/Persistence/Factories/UserFactory.cs | 4 +--- src/Umbraco.Web/Security/WebSecurity.cs | 1 - src/umbraco.businesslogic/UserType.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 305c630a71..1c3ad314e5 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -32,9 +32,7 @@ namespace Umbraco.Core.Persistence.Factories IsLockedOut = dto.NoConsole, IsApproved = dto.Disabled == false, Email = dto.Email, - Language = dto.UserLanguage, - //NOTE: The default permission come from the user type's default permissions - DefaultPermissions = _userType.Permissions + Language = dto.UserLanguage }; foreach (var app in dto.User2AppDtos) diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 5239207316..5896a3f05b 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -222,7 +222,6 @@ namespace Umbraco.Web.Security Language = GlobalSettings.DefaultUILanguage, Name = membershipUser.UserName, RawPasswordValue = Guid.NewGuid().ToString("N"), //Need to set this to something - will not be used though - DefaultPermissions = writer.Permissions, Username = membershipUser.UserName, StartContentId = -1, StartMediaId = -1, diff --git a/src/umbraco.businesslogic/UserType.cs b/src/umbraco.businesslogic/UserType.cs index 31604c3ce9..297695761e 100644 --- a/src/umbraco.businesslogic/UserType.cs +++ b/src/umbraco.businesslogic/UserType.cs @@ -122,7 +122,7 @@ namespace umbraco.BusinessLogic /// public string DefaultPermissions { - get { return string.Join("", UserTypeItem.Permissions); } + get { return UserTypeItem.Permissions == null ? string.Empty : string.Join("", UserTypeItem.Permissions); } set { UserTypeItem.Permissions = value.ToCharArray().Select(x => x.ToString(CultureInfo.InvariantCulture)); } } From 74d0ec949d67327a08b3f803f06a0d30dbfaaeca Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 3 Mar 2015 19:43:00 +0100 Subject: [PATCH 023/249] Delete bower_components folder before build else missing packages will sometimes not be downloaded --- build/Build.bat | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/Build.bat b/build/Build.bat index 0b30dc6a2e..98b8adad2f 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -29,8 +29,9 @@ SET nuGetFolder=%CD%\..\src\packages\ ..\src\.nuget\NuGet.exe install ..\src\umbraco.businesslogic\packages.config -OutputDirectory %nuGetFolder% ..\src\.nuget\NuGet.exe install ..\src\Umbraco.Core\packages.config -OutputDirectory %nuGetFolder% -ECHO Removing the belle build folder to make sure everything is clean as a whistle +ECHO Removing the belle build folder and bower_components folder to make sure everything is clean as a whistle RD ..\src\Umbraco.Web.UI.Client\build /Q /S +RD ..\src\Umbraco.Web.UI.Client\bower_components /Q /S ECHO Removing existing built files to make sure everything is clean as a whistle RMDIR /Q /S _BuildOutput From 97a085d12b01d9e477c7b8d7acc7d2edc790fe5f Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 3 Mar 2015 20:19:41 +0100 Subject: [PATCH 024/249] Bump version --- build/UmbracoVersion.txt | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 654fc8d0eb..0b0f35270e 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.2 \ No newline at end of file +7.2.3 \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index dfc8d6931e..b5b9e206b5 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.2.2"); + private static readonly Version Version = new Version("7.2.3"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index b9af07a7bb..3635e5174d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2547,9 +2547,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7220 + 7230 / - http://localhost:7220 + http://localhost:7230 False False From 88ae95150edcd36231e68e0a5cbb8cde6cf26ea0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 5 Mar 2015 16:36:06 +1100 Subject: [PATCH 025/249] Fixes: U4-6342 UmbracoEnsuredPage doesn't work in 7.2.2 --- .../UI/Pages/UmbracoEnsuredPage.cs | 15 +++++++++++++ .../BasePages/UmbracoEnsuredPage.cs | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs index 6adcb6cf2b..c09fd0fa0c 100644 --- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs +++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs @@ -8,6 +8,7 @@ using umbraco; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; using Umbraco.Core; +using Umbraco.Core.Security; namespace Umbraco.Web.UI.Pages { @@ -37,10 +38,24 @@ namespace Umbraco.Web.UI.Pages /// Authorizes the user /// /// + /// + /// Checks if the page exists outside of the /umbraco route, in which case the request will not have been authenticated for the back office + /// so we'll force authentication. + /// protected override void OnPreInit(EventArgs e) { base.OnPreInit(e); + //If this is not a back office request, then the module won't have authenticated it, in this case we + // need to do the auth manually and since this is an UmbracoEnsuredPage, this is the anticipated behavior + // TODO: When we implement Identity, this process might not work anymore, will be an interesting challenge + if (Context.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false) + { + var http = new HttpContextWrapper(Context); + var ticket = http.GetUmbracoAuthTicket(); + http.AuthenticateCurrentRequest(ticket, true); + } + try { Security.ValidateCurrentUser(true); diff --git a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs index af2d9bf5aa..58362d50f2 100644 --- a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs +++ b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs @@ -1,11 +1,13 @@ using System; using Umbraco.Core.Logging; using System.Linq; +using System.Web; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; +using Umbraco.Core.Security; namespace umbraco.BasePages { @@ -15,6 +17,25 @@ namespace umbraco.BasePages [Obsolete("This class has been superceded by Umbraco.Web.UI.Pages.UmbracoEnsuredPage")] public class UmbracoEnsuredPage : BasePage { + /// + /// Checks if the page exists outside of the /umbraco route, in which case the request will not have been authenticated for the back office + /// so we'll force authentication. + /// + /// + protected override void OnPreInit(EventArgs e) + { + base.OnPreInit(e); + + //If this is not a back office request, then the module won't have authenticated it, in this case we + // need to do the auth manually and since this is an UmbracoEnsuredPage, this is the anticipated behavior + // TODO: When we implement Identity, this process might not work anymore, will be an interesting challenge + if (Context.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false) + { + var http = new HttpContextWrapper(Context); + var ticket = http.GetUmbracoAuthTicket(); + http.AuthenticateCurrentRequest(ticket, true); + } + } /// /// Gets/sets the app for which this page belongs to so that we can validate the current user's security against it From b7bb98d824df44b4f2171682b65991d553e5aaf7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 5 Mar 2015 17:04:02 +1100 Subject: [PATCH 026/249] fixes localized text request when an invalid cookie is present --- src/Umbraco.Web/Editors/BackOfficeController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 0cdebea26d..b353b3ce27 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -15,6 +15,7 @@ using Umbraco.Core.IO; using Umbraco.Core.Manifest; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.Trees; @@ -62,7 +63,9 @@ namespace Umbraco.Web.Editors { var cultureInfo = culture == null //if the user is logged in, get their culture, otherwise default to 'en' - ? User.Identity.IsAuthenticated ? Security.CurrentUser.GetUserCulture(Services.TextService) : CultureInfo.GetCultureInfo("en") + ? User.Identity.IsAuthenticated && User.Identity is UmbracoBackOfficeIdentity + ? Security.CurrentUser.GetUserCulture(Services.TextService) + : CultureInfo.GetCultureInfo("en") : CultureInfo.GetCultureInfo(culture); var textForCulture = Services.TextService.GetAllStoredValues(cultureInfo) From 46212904ef797f398aba16cd3ed6f97c2745b77a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 5 Mar 2015 17:08:58 +1100 Subject: [PATCH 027/249] Changes the SetPropertyValueAndDetectChanges to throw an exception if an Enumerable is passed in without using the overload the specifies an IEqualityComparer. Updates all entities that use this method with IEnumerables to specify the correct Equality comparer which now uses the new UnsortedSequenceEqual method. Added unit tests for new EnumerableExtensions and makes the ContainsAll work faster. --- src/Umbraco.Core/EnumerableExtensions.cs | 43 ++++++++++++++----- src/Umbraco.Core/Models/ContentType.cs | 6 ++- src/Umbraco.Core/Models/ContentTypeBase.cs | 6 ++- src/Umbraco.Core/Models/DictionaryItem.cs | 6 ++- .../EntityBase/TracksChangesEntityBase.cs | 34 ++++++++++++++- .../Models/Membership/UserType.cs | 7 ++- src/Umbraco.Core/Models/Property.cs | 21 ++++++++- .../EnumerableExtensionsTests.cs | 29 +++++++++++++ 8 files changed, 134 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs index 8055b33ab8..79b48af3ab 100644 --- a/src/Umbraco.Core/EnumerableExtensions.cs +++ b/src/Umbraco.Core/EnumerableExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -114,13 +115,7 @@ namespace Umbraco.Core /// public static bool ContainsAll(this IEnumerable source, IEnumerable other) { - var matches = true; - foreach (var i in other) - { - matches = source.Contains(i); - if (!matches) break; - } - return matches; + return other.Except(source).Any() == false; } /// @@ -132,7 +127,7 @@ namespace Umbraco.Core /// public static bool ContainsAny(this IEnumerable source, IEnumerable other) { - return other.Any(i => source.Contains(i)); + return other.Any(source.Contains); } /// @@ -224,7 +219,7 @@ namespace Umbraco.Core return sequence.Select( x => { - if (typeof(TActual).IsAssignableFrom(x.GetType())) + if (x is TActual) { var casted = x as TActual; projection.Invoke(casted); @@ -276,6 +271,34 @@ namespace Umbraco.Core ///The enumerable to search. ///The item to find. ///The index of the first matching item, or -1 if the item was not found. - public static int IndexOf(this IEnumerable items, T item) { return items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); } + public static int IndexOf(this IEnumerable items, T item) + { + return items.FindIndex(i => EqualityComparer.Default.Equals(item, i)); + } + + /// + /// Determines if 2 lists have equal elements within them regardless of how they are sorted + /// + /// + /// + /// + /// + /// + /// The logic for this is taken from: + /// http://stackoverflow.com/questions/4576723/test-whether-two-ienumerablet-have-the-same-values-with-the-same-frequencies + /// + /// There's a few answers, this one seems the best for it's simplicity and based on the comment of Eamon + /// + public static bool UnsortedSequenceEqual(this IEnumerable source, IEnumerable other) + { + if (source == null && other == null) return true; + if (source == null || other == null) return false; + + var list1Groups = source.ToLookup(i => i); + var list2Groups = other.ToLookup(i => i); + return list1Groups.Count == list2Groups.Count + && list1Groups.All(g => g.Count() == list2Groups[g.Key].Count()); + } + } } diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index fc53a21c3f..355d724fbe 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -90,7 +90,11 @@ namespace Umbraco.Core.Models { _allowedTemplates = value; return _allowedTemplates; - }, _allowedTemplates, AllowedTemplatesSelector); + }, _allowedTemplates, AllowedTemplatesSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (templates, enumerable) => templates.UnsortedSequenceEqual(enumerable), + templates => templates.GetHashCode())); } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 07c2067022..bee9ed9b6e 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -351,7 +351,11 @@ namespace Umbraco.Core.Models { _allowedContentTypes = value; return _allowedContentTypes; - }, _allowedContentTypes, AllowedContentTypesSelector); + }, _allowedContentTypes, AllowedContentTypesSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (sorts, enumerable) => sorts.UnsortedSequenceEqual(enumerable), + sorts => sorts.GetHashCode())); } } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index 6c2ee47714..17cc744bd5 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -80,7 +80,11 @@ namespace Umbraco.Core.Models { _translations = value; return _translations; - }, _translations, TranslationsSelector); + }, _translations, TranslationsSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (enumerable, translations) => enumerable.UnsortedSequenceEqual(translations), + enumerable => enumerable.GetHashCode())); } } diff --git a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs index 7ab1b47ebb..cd5b762f84 100644 --- a/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs +++ b/src/Umbraco.Core/Models/EntityBase/TracksChangesEntityBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -166,7 +167,36 @@ namespace Umbraco.Core.Models.EntityBase /// save a document type, nearly all properties are flagged as dirty just because we've 'reset' them, but they are all set /// to the same value, so it's really not dirty. /// - internal virtual bool SetPropertyValueAndDetectChanges(Func setValue, T value, PropertyInfo propertySelector) + internal bool SetPropertyValueAndDetectChanges(Func setValue, T value, PropertyInfo propertySelector) + { + if ((typeof(T) == typeof(string) == false) && TypeHelper.IsTypeAssignableFrom(typeof(T))) + { + throw new InvalidOperationException("This method does not support IEnumerable instances. For IEnumerable instances a manual custom equality check will be required"); + } + + return SetPropertyValueAndDetectChanges(setValue, value, propertySelector, + new DelegateEqualityComparer( + //Standard Equals comparison + (arg1, arg2) => Equals(arg1, arg2), + arg => arg.GetHashCode())); + + } + + /// + /// Used by inheritors to set the value of properties, this will detect if the property value actually changed and if it did + /// it will ensure that the property has a dirty flag set. + /// + /// + /// + /// + /// The equality comparer to use + /// returns true if the value changed + /// + /// This is required because we don't want a property to show up as "dirty" if the value is the same. For example, when we + /// save a document type, nearly all properties are flagged as dirty just because we've 'reset' them, but they are all set + /// to the same value, so it's really not dirty. + /// + internal bool SetPropertyValueAndDetectChanges(Func setValue, T value, PropertyInfo propertySelector, IEqualityComparer comparer) { var initVal = value; var newVal = setValue(value); @@ -174,7 +204,7 @@ namespace Umbraco.Core.Models.EntityBase //don't track changes, just set the value (above) if (_changeTrackingEnabled == false) return false; - if (Equals(initVal, newVal) == false) + if (comparer.Equals(initVal, newVal) == false) { OnPropertyChanged(propertySelector); return true; diff --git a/src/Umbraco.Core/Models/Membership/UserType.cs b/src/Umbraco.Core/Models/Membership/UserType.cs index 915943be43..5c32e57f38 100644 --- a/src/Umbraco.Core/Models/Membership/UserType.cs +++ b/src/Umbraco.Core/Models/Membership/UserType.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -67,7 +68,11 @@ namespace Umbraco.Core.Models.Membership { _permissions = value; return _permissions; - }, _permissions, PermissionsSelector); + }, _permissions, PermissionsSelector, + //Custom comparer for enumerable + new DelegateEqualityComparer>( + (enum1, enum2) => enum1.UnsortedSequenceEqual(enum2), + enum1 => enum1.GetHashCode())); } } } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index 4f8fa3deb2..8ba30665d0 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -1,4 +1,7 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; using Umbraco.Core.Models.EntityBase; @@ -120,7 +123,7 @@ namespace Umbraco.Core.Models { bool typeValidation = _propertyType.IsPropertyTypeValid(value); - if (!typeValidation) + if (typeValidation == false) throw new Exception( string.Format( "Type validation failed. The value type: '{0}' does not match the DataType in PropertyType with alias: '{1}'", @@ -130,7 +133,21 @@ namespace Umbraco.Core.Models { _value = value; return _value; - }, _value, ValueSelector); + }, _value, ValueSelector, + new DelegateEqualityComparer( + (o, o1) => + { + //Custom comparer for enumerable if it is enumerable + if (o == null && o1 == null) return true; + if (o == null || o1 == null) return false; + var enum1 = o as IEnumerable; + var enum2 = o1 as IEnumerable; + if (enum1 != null && enum2 != null) + { + return enum1.Cast().UnsortedSequenceEqual(enum2.Cast()); + } + return o.Equals(o1); + }, o => o.GetHashCode())); } } diff --git a/src/Umbraco.Tests/EnumerableExtensionsTests.cs b/src/Umbraco.Tests/EnumerableExtensionsTests.cs index 0fd436fcea..bbc0420ccc 100644 --- a/src/Umbraco.Tests/EnumerableExtensionsTests.cs +++ b/src/Umbraco.Tests/EnumerableExtensionsTests.cs @@ -11,6 +11,35 @@ namespace Umbraco.Tests public class EnumerableExtensionsTests { + [Test] + public void Unsorted_Sequence_Equal() + { + var list1 = new[] { 1, 2, 3, 4, 5, 6 }; + var list2 = new[] { 6, 5, 3, 2, 1, 4 }; + var list3 = new[] { 6, 5, 4, 3, 2, 2 }; + + Assert.IsTrue(list1.UnsortedSequenceEqual(list2)); + Assert.IsTrue(list2.UnsortedSequenceEqual(list1)); + Assert.IsFalse(list1.UnsortedSequenceEqual(list3)); + + Assert.IsTrue(((IEnumerable)null).UnsortedSequenceEqual(null)); + Assert.IsFalse(((IEnumerable)null).UnsortedSequenceEqual(list1)); + Assert.IsFalse(list1.UnsortedSequenceEqual(null)); + } + + [Test] + public void Contains_All() + { + var list1 = new[] {1, 2, 3, 4, 5, 6}; + var list2 = new[] {6, 5, 3, 2, 1, 4}; + var list3 = new[] {6, 5, 4, 3}; + + Assert.IsTrue(list1.ContainsAll(list2)); + Assert.IsTrue(list2.ContainsAll(list1)); + Assert.IsTrue(list1.ContainsAll(list3)); + Assert.IsFalse(list3.ContainsAll(list1)); + } + [Test] public void Flatten_List_2() { From 99598ec0609b3676aee40301b587344867e7ddd3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 3 Mar 2015 13:03:11 +0100 Subject: [PATCH 028/249] U4-3753 - add a way to get the rendering culture of a content --- src/Umbraco.Web/Models/ContentExtensions.cs | 42 +++++++++++++++++++ src/Umbraco.Web/PublishedContentExtensions.cs | 36 ++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Umbraco.Web/Models/ContentExtensions.cs diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs new file mode 100644 index 0000000000..85f1a3033d --- /dev/null +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Models +{ + public static class ContentExtensions + { + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// The content. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(this IContent content, Uri current = null) + { + var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + var pos = route.IndexOf('/'); + + var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + + var domain = pos == 0 + ? null + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + + if (domain == null) + { + var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); + } + + var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + return wcDomain == null + ? new CultureInfo(domain.Language.IsoCode) + : new CultureInfo(wcDomain.Language.IsoCode); + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d7a19288fc..923edf50c2 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Web; using Examine.LuceneEngine.SearchCriteria; @@ -8,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; using Umbraco.Core; +using Umbraco.Web.Routing; using ContentType = umbraco.cms.businesslogic.ContentType; namespace Umbraco.Web @@ -1882,5 +1884,39 @@ namespace Umbraco.Web } #endregion + + #region Culture + + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// The content. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) + { + var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + var pos = route.IndexOf('/'); + + var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + + var domain = pos == 0 + ? null + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + + if (domain == null) + { + var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); + } + + var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + return wcDomain == null + ? new CultureInfo(domain.Language.IsoCode) + : new CultureInfo(wcDomain.Language.IsoCode); + } + + #endregion } } From 46fdd605ae8a7141eb976f91d03374de9698c244 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 4 Mar 2015 12:16:28 +0100 Subject: [PATCH 029/249] Refactor distributed cache & merge Shazwazza's DataBaseServerMessenger --- src/Umbraco.Core/CoreBootManager.cs | 2 +- .../Logging/AppDomainTokenFormatter.cs | 21 + .../Models/Rdbms/CacheInstructionDto.cs | 31 + .../Models/Rdbms/ServerRegistrationDto.cs | 11 +- src/Umbraco.Core/Models/ServerRegistration.cs | 92 +-- .../Factories/ServerRegistrationFactory.cs | 16 +- .../Mappers/ServerRegistrationMapper.cs | 8 +- .../Initial/DatabaseSchemaCreation.cs | 4 +- .../CreateCacheInstructionTable.cs | 35 ++ .../ServerRegistrationRepository.cs | 7 + .../Services/ServerRegistrationService.cs | 80 ++- src/Umbraco.Core/Services/ServiceContext.cs | 2 +- src/Umbraco.Core/StringExtensions.cs | 12 +- .../Sync/BatchedDatabaseServerMessenger.cs | 75 +++ .../Sync/BatchedWebServiceServerMessenger.cs | 93 +++ src/Umbraco.Core/Sync/ConfigServerAddress.cs | 9 +- .../Sync/ConfigServerRegistrar.cs | 42 +- .../Sync/DatabaseServerMessenger.cs | 385 ++++++++++++ .../Sync/DatabaseServerMessengerOptions.cs | 39 ++ .../Sync/DatabaseServerRegistrar.cs | 24 +- .../Sync/DatabaseServerRegistrarOptions.cs | 29 + .../Sync/DefaultServerMessenger.cs | 552 ------------------ src/Umbraco.Core/Sync/IServerAddress.cs | 5 +- src/Umbraco.Core/Sync/IServerMessenger.cs | 102 ++-- src/Umbraco.Core/Sync/IServerRegistrar.cs | 5 +- src/Umbraco.Core/Sync/MessageType.cs | 2 +- src/Umbraco.Core/Sync/RefreshInstruction.cs | 135 ++++- .../Sync/RefreshInstructionEnvelope.cs | 19 + src/Umbraco.Core/Sync/RefreshMethodType.cs | 44 ++ src/Umbraco.Core/Sync/ServerMessengerBase.cs | 317 ++++++++++ .../Sync/ServerMessengerResolver.cs | 17 +- .../Sync/ServerRegistrarResolver.cs | 19 +- .../Sync/WebServiceServerMessenger.cs | 384 ++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 13 +- .../Cache/CacheRefresherTests.cs | 7 +- .../Persistence/PetaPocoExtensionsTest.cs | 18 +- .../ServerRegistrationRepositoryTest.cs | 8 +- .../BatchedDatabaseServerMessenger.cs | 78 +++ src/Umbraco.Web/BatchedServerMessenger.cs | 326 ----------- .../BatchedWebServiceServerMessenger.cs | 58 ++ src/Umbraco.Web/Cache/DistributedCache.cs | 160 ++--- .../Cache/DistributedCacheExtensions.cs | 489 ++++------------ .../Routing/EnsureRoutableOutcome.cs | 32 +- .../ServerRegistrationEventHandler.cs | 168 ++---- src/Umbraco.Web/Umbraco.Web.csproj | 9 +- src/Umbraco.Web/WebBootManager.cs | 13 +- src/Umbraco.Web/WebServerUtility.cs | 79 +++ src/Umbraco.Web/packages.config | 1 + .../webservices/CacheRefresher.asmx.cs | 261 +++------ src/umbraco.interfaces/ICacheRefresher.cs | 2 + 50 files changed, 2506 insertions(+), 1834 deletions(-) create mode 100644 src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs create mode 100644 src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs create mode 100644 src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs create mode 100644 src/Umbraco.Core/Sync/DatabaseServerMessenger.cs create mode 100644 src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs create mode 100644 src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs delete mode 100644 src/Umbraco.Core/Sync/DefaultServerMessenger.cs create mode 100644 src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs create mode 100644 src/Umbraco.Core/Sync/RefreshMethodType.cs create mode 100644 src/Umbraco.Core/Sync/ServerMessengerBase.cs create mode 100644 src/Umbraco.Core/Sync/WebServiceServerMessenger.cs create mode 100644 src/Umbraco.Web/BatchedDatabaseServerMessenger.cs delete mode 100644 src/Umbraco.Web/BatchedServerMessenger.cs create mode 100644 src/Umbraco.Web/BatchedWebServiceServerMessenger.cs create mode 100644 src/Umbraco.Web/WebServerUtility.cs diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 4aeee2881d..294ad69a87 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -320,7 +320,7 @@ namespace Umbraco.Core //supplying a username/password, this will automatically disable distributed calls // .. we'll override this in the WebBootManager ServerMessengerResolver.Current = new ServerMessengerResolver( - new DefaultServerMessenger()); + new WebServiceServerMessenger()); MappingResolver.Current = new MappingResolver( ServiceProvider, LoggerResolver.Current.Logger, diff --git a/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs b/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs new file mode 100644 index 0000000000..0abddc63e3 --- /dev/null +++ b/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; + +namespace Umbraco.Core.Logging +{ + /// + /// Allows for outputting a normalized appdomainappid token in a log format + /// + public sealed class AppDomainTokenConverter : log4net.Util.PatternConverter + { + protected override void Convert(TextWriter writer, object state) + { + writer.Write(HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty)); + } + } +} diff --git a/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs b/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs new file mode 100644 index 0000000000..c24004b7dc --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/CacheInstructionDto.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoCacheInstruction")] + [PrimaryKey("id")] + [ExplicitColumns] + internal class CacheInstructionDto + { + [Column("id")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [PrimaryKeyColumn(AutoIncrement = true, Name = "PK_umbracoCacheInstruction")] + public int Id { get; set; } + + [Column("utcStamp")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public DateTime UtcStamp { get; set; } + + [Column("jsonInstruction")] + [SpecialDbType(SpecialDbTypes.NTEXT)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Instructions { get; set; } + + [Column("originated")] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Length(500)] + public string OriginIdentity { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs index 9833d9ab74..b7bdf265ce 100644 --- a/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/ServerRegistrationDto.cs @@ -15,22 +15,19 @@ namespace Umbraco.Core.Models.Rdbms [Column("address")] [Length(500)] - public string Address { get; set; } + public string ServerAddress { get; set; } - /// - /// A unique column in the database, a computer name must always be unique! - /// [Column("computerName")] [Length(255)] - [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] - public string ComputerName { get; set; } + [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique + public string ServerIdentity { get; set; } [Column("registeredDate")] [Constraint(Default = "getdate()")] public DateTime DateRegistered { get; set; } [Column("lastNotifiedDate")] - public DateTime LastNotified { get; set; } + public DateTime DateAccessed { get; set; } [Column("isActive")] [Index(IndexTypes.NonClustered)] diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs index 7f43f5dfd2..900d6deb94 100644 --- a/src/Umbraco.Core/Models/ServerRegistration.cs +++ b/src/Umbraco.Core/Models/ServerRegistration.cs @@ -2,61 +2,67 @@ using System.Globalization; using System.Reflection; using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Persistence.Mappers; using Umbraco.Core.Sync; namespace Umbraco.Core.Models { - internal class ServerRegistration : Entity, IServerAddress, IAggregateRoot + /// + /// Represents a registered server in a multiple-servers environment. + /// + public class ServerRegistration : Entity, IServerAddress, IAggregateRoot { private string _serverAddress; - private string _computerName; + private string _serverIdentity; private bool _isActive; private static readonly PropertyInfo ServerAddressSelector = ExpressionHelper.GetPropertyInfo(x => x.ServerAddress); - private static readonly PropertyInfo ComputerNameSelector = ExpressionHelper.GetPropertyInfo(x => x.ComputerName); + private static readonly PropertyInfo ServerIdentitySelector = ExpressionHelper.GetPropertyInfo(x => x.ServerIdentity); private static readonly PropertyInfo IsActiveSelector = ExpressionHelper.GetPropertyInfo(x => x.IsActive); + /// + /// Initialiazes a new instance of the class. + /// public ServerRegistration() - { - - } + { } /// - /// Creates an item with pre-filled properties + /// Initialiazes a new instance of the class. /// - /// - /// - /// - /// - /// - /// - public ServerRegistration(int id, string serverAddress, string computerName, DateTime createDate, DateTime updateDate, bool isActive) + /// The unique id of the server registration. + /// The server url. + /// The unique server identity. + /// The date and time the registration was created. + /// The date and time the registration was last accessed. + /// A value indicating whether the registration is active. + public ServerRegistration(int id, string serverAddress, string serverIdentity, DateTime registered, DateTime accessed, bool isActive) { - UpdateDate = updateDate; - CreateDate = createDate; - Key = Id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); + UpdateDate = accessed; + CreateDate = registered; + Key = id.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); Id = id; ServerAddress = serverAddress; - ComputerName = computerName; + ServerIdentity = serverIdentity; IsActive = isActive; } /// - /// Creates a new instance for persisting a new item + /// Initialiazes a new instance of the class. /// - /// - /// - /// - public ServerRegistration(string serverAddress, string computerName, DateTime createDate) + /// The server url. + /// The unique server identity. + /// The date and time the registration was created. + public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered) { - CreateDate = createDate; - UpdateDate = createDate; + CreateDate = registered; + UpdateDate = registered; Key = 0.ToString(CultureInfo.InvariantCulture).EncodeAsGuid(); ServerAddress = serverAddress; - ComputerName = computerName; + ServerIdentity = serverIdentity; } + /// + /// Gets or sets the server url. + /// public string ServerAddress { get { return _serverAddress; } @@ -70,19 +76,25 @@ namespace Umbraco.Core.Models } } - public string ComputerName + /// + /// Gets or sets the server unique identity. + /// + public string ServerIdentity { - get { return _computerName; } + get { return _serverIdentity; } set { SetPropertyValueAndDetectChanges(o => { - _computerName = value; - return _computerName; - }, _computerName, ComputerNameSelector); + _serverIdentity = value; + return _serverIdentity; + }, _serverIdentity, ServerIdentitySelector); } } + /// + /// Gets or sets a value indicating whether the server is active. + /// public bool IsActive { get { return _isActive; } @@ -96,9 +108,23 @@ namespace Umbraco.Core.Models } } + /// + /// Gets the date and time the registration was created. + /// + public DateTime Registered { get { return CreateDate; } set { CreateDate = value; }} + + /// + /// Gets the date and time the registration was last accessed. + /// + public DateTime Accessed { get { return UpdateDate; } set { UpdateDate = value; }} + + /// + /// Converts the value of this instance to its equivalent string representation. + /// + /// public override string ToString() { - return "(" + ServerAddress + ", " + ComputerName + ", IsActive = " + IsActive + ")"; + return string.Format("{{\"{0}\", \"{1}\", {2}active}}", ServerAddress, ServerIdentity, IsActive ? "" : "!"); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs b/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs index e13b24e1e1..aa0ed25ccd 100644 --- a/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ServerRegistrationFactory.cs @@ -1,5 +1,4 @@ -using System.Globalization; -using Umbraco.Core.Models; +using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Factories @@ -10,7 +9,7 @@ namespace Umbraco.Core.Persistence.Factories public ServerRegistration BuildEntity(ServerRegistrationDto dto) { - var model = new ServerRegistration(dto.Id, dto.Address, dto.ComputerName, dto.DateRegistered, dto.LastNotified, dto.IsActive); + var model = new ServerRegistration(dto.Id, dto.ServerAddress, dto.ServerIdentity, dto.DateRegistered, dto.DateAccessed, dto.IsActive); //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 model.ResetDirtyProperties(false); @@ -19,16 +18,17 @@ namespace Umbraco.Core.Persistence.Factories public ServerRegistrationDto BuildDto(ServerRegistration entity) { - var dto = new ServerRegistrationDto() + var dto = new ServerRegistrationDto { - Address = entity.ServerAddress, + ServerAddress = entity.ServerAddress, DateRegistered = entity.CreateDate, IsActive = entity.IsActive, - LastNotified = entity.UpdateDate, - ComputerName = entity.ComputerName + DateAccessed = entity.UpdateDate, + ServerIdentity = entity.ServerIdentity }; + if (entity.HasIdentity) - dto.Id = int.Parse(entity.Id.ToString(CultureInfo.InvariantCulture)); + dto.Id = entity.Id; return dto; } diff --git a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs index 24b0632f8a..40a18adc59 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ServerRegistrationMapper.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Models.Rdbms; namespace Umbraco.Core.Persistence.Mappers { [MapperFor(typeof(ServerRegistration))] - public sealed class ServerRegistrationMapper : BaseMapper + internal sealed class ServerRegistrationMapper : BaseMapper { private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); @@ -29,10 +29,10 @@ namespace Umbraco.Core.Persistence.Mappers { CacheMap(src => src.Id, dto => dto.Id); CacheMap(src => src.IsActive, dto => dto.IsActive); - CacheMap(src => src.ServerAddress, dto => dto.Address); + CacheMap(src => src.ServerAddress, dto => dto.ServerAddress); CacheMap(src => src.CreateDate, dto => dto.DateRegistered); - CacheMap(src => src.UpdateDate, dto => dto.LastNotified); - CacheMap(src => src.ComputerName, dto => dto.ComputerName); + CacheMap(src => src.UpdateDate, dto => dto.DateAccessed); + CacheMap(src => src.ServerIdentity, dto => dto.ServerIdentity); } #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index b77e0843ea..76689948c5 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -79,9 +79,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {38, typeof (User2NodeNotifyDto)}, {39, typeof (User2NodePermissionDto)}, {40, typeof (ServerRegistrationDto)}, - {41, typeof (AccessDto)}, - {42, typeof (AccessRuleDto)} + {42, typeof (AccessRuleDto)}, + {43, typeof(CacheInstructionDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs new file mode 100644 index 0000000000..66391f9fe5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/CreateCacheInstructionTable.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 1, GlobalSettings.UmbracoMigrationName)] + public class CreateCacheInstructionTable : MigrationBase + { + public override void Up() + { + var textType = SqlSyntaxContext.SqlSyntaxProvider.GetSpecialDbType(SpecialDbTypes.NTEXT); + + Create.Table("umbracoCacheInstruction") + .WithColumn("id").AsInt32().Identity().NotNullable() + .WithColumn("utcStamp").AsDateTime().NotNullable() + .WithColumn("jsonInstruction").AsCustom(textType).NotNullable(); + + Create.PrimaryKey("PK_umbracoCacheInstruction") + .OnTable("umbracoCacheInstruction") + .Column("id"); + } + + public override void Down() + { + Delete.PrimaryKey("PK_umbracoCacheInstruction").FromTable("cmsContentType2ContentType"); + Delete.Table("cmsContentType2ContentType"); + } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs index 020ae2e9cf..1ef87357ea 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ServerRegistrationRepository.cs @@ -125,5 +125,12 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + var timeoutDate = DateTime.UtcNow.Subtract(staleTimeout); + + Database.Update("SET isActive=0 WHERE lastNotifiedDate < @timeoutDate", new {timeoutDate = timeoutDate}); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ServerRegistrationService.cs b/src/Umbraco.Core/Services/ServerRegistrationService.cs index f52c05768f..157e7b795d 100644 --- a/src/Umbraco.Core/Services/ServerRegistrationService.cs +++ b/src/Umbraco.Core/Services/ServerRegistrationService.cs @@ -11,63 +11,66 @@ namespace Umbraco.Core.Services { /// - /// Service to manage server registrations in the database + /// Manages server registrations in the database. /// - internal class ServerRegistrationService : RepositoryService + public sealed class ServerRegistrationService : RepositoryService { - - public ServerRegistrationService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger) - : base(provider, repositoryFactory, logger) - { - } + /// + /// Initializes a new instance of the class. + /// + /// A UnitOfWork provider. + /// A repository factory. + /// A logger. + public ServerRegistrationService(IDatabaseUnitOfWorkProvider uowProvider, RepositoryFactory repositoryFactory, ILogger logger) + : base(uowProvider, repositoryFactory, logger) + { } /// - /// Called to 'call home' to ensure the current server has an active record + /// Touches a server to mark it as active; deactivate stale servers. /// - /// - public void EnsureActive(string address) + /// The server url. + /// The server unique identity. + /// The time after which a server is considered stale. + public void TouchServer(string serverAddress, string serverIdentity, TimeSpan staleTimeout) { - var uow = UowProvider.GetUnitOfWork(); using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) { - //NOTE: we cannot use Environment.MachineName as this does not work in medium trust - // found this out in CDF a while back: http://clientdependency.codeplex.com/workitem/13191 - - var computerName = System.Net.Dns.GetHostName(); - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper()); - var found = repo.GetByQuery(query).ToArray(); - ServerRegistration server; - if (found.Any()) + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); + var server = repo.GetByQuery(query).FirstOrDefault(); + if (server == null) { - server = found.First(); - server.ServerAddress = address; //This should not really change but it might! - server.UpdateDate = DateTime.UtcNow; //Stick with Utc dates since these might be globally distributed - server.IsActive = true; + server = new ServerRegistration(serverAddress, serverIdentity, DateTime.UtcNow) + { + IsActive = true + }; } else { - server = new ServerRegistration(address, computerName, DateTime.UtcNow); + server.ServerAddress = serverAddress; // should not really change but it might! + server.UpdateDate = DateTime.UtcNow; // stick with Utc dates since these might be globally distributed + server.IsActive = true; } repo.AddOrUpdate(server); uow.Commit(); + + repo.DeactiveStaleServers(staleTimeout); } } /// - /// Deactivates a server by name + /// Deactivates a server. /// - /// - public void DeactiveServer(string computerName) + /// The server unique identity. + public void DeactiveServer(string serverIdentity) { var uow = UowProvider.GetUnitOfWork(); using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) { - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == computerName.ToUpper()); - var found = repo.GetByQuery(query).ToArray(); - if (found.Any()) + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == serverIdentity.ToUpper()); + var server = repo.GetByQuery(query).FirstOrDefault(); + if (server != null) { - var server = found.First(); server.IsActive = false; repo.AddOrUpdate(server); uow.Commit(); @@ -76,7 +79,20 @@ namespace Umbraco.Core.Services } /// - /// Return all active servers + /// Deactivates stale servers. + /// + /// The time after which a server is considered stale. + public void DeactiveStaleServers(TimeSpan staleTimeout) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateServerRegistrationRepository(uow)) + { + repo.DeactiveStaleServers(staleTimeout); + } + } + + /// + /// Return all active servers. /// /// public IEnumerable GetActiveServers() diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f1eb873db7..255f6e457f 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -274,7 +274,7 @@ namespace Umbraco.Core.Services /// /// Gets the /// - internal ServerRegistrationService ServerRegistrationService + public ServerRegistrationService ServerRegistrationService { get { return _serverRegistrationService.Value; } } diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 86a2f9c36e..eb7c0f4975 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -114,13 +114,11 @@ namespace Umbraco.Core internal static string ReplaceNonAlphanumericChars(this string input, char replacement) { - //any character that is not alphanumeric, convert to a hyphen - var mName = input; - foreach (var c in mName.ToCharArray().Where(c => !char.IsLetterOrDigit(c))) - { - mName = mName.Replace(c, replacement); - } - return mName; + var inputArray = input.ToCharArray(); + var outputArray = new char[input.Length]; + for (var i = 0; i < inputArray.Length; i++) + outputArray[i] = char.IsLetterOrDigit(inputArray[i]) ? inputArray[i] : replacement; + return new string(outputArray); } /// diff --git a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs new file mode 100644 index 0000000000..5c439377f3 --- /dev/null +++ b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Umbraco.Core.Models.Rdbms; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + // abstract because it needs to be inherited by a class that will + // - trigger FlushBatch() when appropriate + // - trigger Boot() when appropriate + // - trigger Sync() when appropriate + // + public abstract class BatchedDatabaseServerMessenger : DatabaseServerMessenger + { + private readonly Func> _getBatch; + + protected BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options, + Func> getBatch) + : base(appContext, enableDistCalls, options) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + public void FlushBatch() + { + var batch = _getBatch(false); + if (batch == null) return; + + var instructions = batch.SelectMany(x => x.Instructions).ToArray(); + batch.Clear(); + if (instructions.Length == 0) return; + + var dto = new CacheInstructionDto + { + UtcStamp = DateTime.UtcNow, + Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), + OriginIdentity = GetLocalIdentity() + }; + + ApplicationContext.DatabaseContext.Database.Insert(dto); + } + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + BatchMessage(servers, refresher, messageType, idsA, arrayType, json); + } + + protected void BatchMessage( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idType = null, + string json = null) + { + var batch = _getBatch(true); + if (batch == null) + throw new Exception("Failed to get a batch."); + + batch.Add(new RefreshInstructionEnvelope(servers, refresher, + RefreshInstruction.GetInstructions(refresher, messageType, ids, idType, json))); + } + } +} diff --git a/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs new file mode 100644 index 0000000000..57dd273f2a --- /dev/null +++ b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + // abstract because it needs to be inherited by a class that will + // - implement ProcessBatch() + // - trigger FlushBatch() when appropriate + // + internal abstract class BatchedWebServiceServerMessenger : WebServiceServerMessenger + { + private readonly Func> _getBatch; + + internal BatchedWebServiceServerMessenger(Func> getBatch) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + internal BatchedWebServiceServerMessenger(string login, string password, Func> getBatch) + : base(login, password) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls, Func> getBatch) + : base(login, password, useDistributedCalls) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + protected BatchedWebServiceServerMessenger(Func> getLoginAndPassword, Func> getBatch) + : base(getLoginAndPassword) + { + if (getBatch == null) + throw new ArgumentNullException("getBatch"); + + _getBatch = getBatch; + } + + protected void FlushBatch() + { + var batch = _getBatch(false); + if (batch == null) return; + + var batcha = batch.ToArray(); + batch.Clear(); + if (batcha.Length == 0) return; + + ProcessBatch(batcha); + } + + // needs to be overriden to actually do something + protected abstract void ProcessBatch(RefreshInstructionEnvelope[] batch); + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + BatchMessage(servers, refresher, messageType, idsA, arrayType, json); + } + + protected void BatchMessage( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idType = null, + string json = null) + { + var batch = _getBatch(true); + if (batch == null) + throw new Exception("Failed to get a batch."); + + batch.Add(new RefreshInstructionEnvelope(servers, refresher, + RefreshInstruction.GetInstructions(refresher, messageType, ids, idType, json))); + } + } +} diff --git a/src/Umbraco.Core/Sync/ConfigServerAddress.cs b/src/Umbraco.Core/Sync/ConfigServerAddress.cs index 1bfa7a305e..431dcd9573 100644 --- a/src/Umbraco.Core/Sync/ConfigServerAddress.cs +++ b/src/Umbraco.Core/Sync/ConfigServerAddress.cs @@ -1,16 +1,14 @@ -using System.Xml; -using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; namespace Umbraco.Core.Sync { /// - /// A server registration based on the legacy umbraco xml configuration in umbracoSettings + /// Provides the address of a server based on the Xml configuration. /// internal class ConfigServerAddress : IServerAddress { - public ConfigServerAddress(IServer n) { var webServicesUrl = IOHelper.ResolveUrl(SystemDirectories.WebServices); @@ -29,7 +27,6 @@ namespace Umbraco.Core.Sync public override string ToString() { return ServerAddress; - } - + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs index 69f54fcab7..06cf03f7ce 100644 --- a/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/ConfigServerRegistrar.cs @@ -1,53 +1,35 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Xml; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Models; namespace Umbraco.Core.Sync { /// - /// A registrar that uses the legacy xml configuration in umbracoSettings to get a list of defined server nodes + /// Provides server registrations to the distributed cache by reading the legacy Xml configuration + /// in umbracoSettings to get the list of (manually) configured server nodes. /// internal class ConfigServerRegistrar : IServerRegistrar { - private readonly IEnumerable _servers; + private readonly List _addresses; public ConfigServerRegistrar() : this(UmbracoConfig.For.UmbracoSettings().DistributedCall.Servers) - { - - } + { } internal ConfigServerRegistrar(IEnumerable servers) { - _servers = servers; + _addresses = servers == null + ? new List() + : servers + .Select(x => new ConfigServerAddress(x)) + .Cast() + .ToList(); } - private List _addresses; - public IEnumerable Registrations { - get - { - if (_addresses == null) - { - _addresses = new List(); - - if (_servers != null) - { - foreach (var n in _servers) - { - _addresses.Add(new ConfigServerAddress(n)); - } - } - } - - return _addresses; - } + get { return _addresses; } } } } diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs new file mode 100644 index 0000000000..ed472ea35a --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Web; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// An that works by storing messages in the database. + /// + // + // abstract because it needs to be inherited by a class that will + // - trigger Boot() when appropriate + // - trigger Sync() when appropriate + // + // this messenger writes ALL instructions to the database, + // but only processes instructions coming from remote servers, + // thus ensuring that instructions run only once + // + public abstract class DatabaseServerMessenger : ServerMessengerBase + { + private readonly ApplicationContext _appContext; + private readonly DatabaseServerMessengerOptions _options; + private readonly object _lock = new object(); + private int _lastId = -1; + private volatile bool _syncing; + private DateTime _lastSync; + private bool _initialized; + + protected ApplicationContext ApplicationContext { get { return _appContext; } } + + protected DatabaseServerMessenger(ApplicationContext appContext, bool distributedEnabled, DatabaseServerMessengerOptions options) + : base(distributedEnabled) + { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (options == null) throw new ArgumentNullException("options"); + + _appContext = appContext; + _options = options; + _lastSync = DateTime.UtcNow; + } + + #region Messenger + + protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType) + { + // we don't care if there's servers listed or not, + // if distributed call is enabled we will make the call + return _initialized && DistributedEnabled; + } + + protected override void DeliverRemote( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type idType; + if (GetArrayType(idsA, out idType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + var instructions = RefreshInstruction.GetInstructions(refresher, messageType, idsA, idType, json); + + var dto = new CacheInstructionDto + { + UtcStamp = DateTime.UtcNow, + Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), + OriginIdentity = GetLocalIdentity() + }; + + ApplicationContext.DatabaseContext.Database.Insert(dto); + } + + #endregion + + #region Sync + + /// + /// Boots the messenger. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// Callers MUST ensure thread-safety. + /// + protected void Boot() + { + ReadLastSynced(); + if (_lastId < 0) // never synced before + Initialize(); + } + + /// + /// Initializes a server that has never synchronized before. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void Initialize() + { + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. + LogHelper.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); + + // go get the last id in the db and store it + // note: do it BEFORE initializing otherwise some instructions might get lost + // when doing it before, some instructions might run twice - not an issue + var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + if (lastId > 0) + SaveLastSynced(lastId); + + // execute initializing callbacks + if (_options.InitializingCallbacks != null) + foreach (var callback in _options.InitializingCallbacks) + callback(); + + _initialized = true; + } + + /// + /// Synchronize the server (throttled). + /// + protected void Sync() + { + if ((DateTime.UtcNow - _lastSync).Seconds <= _options.ThrottleSeconds) + return; + + if (_syncing) return; + + lock (_lock) + { + if (_syncing) return; + + _syncing = true; // lock other threads out + _lastSync = DateTime.UtcNow; + + using (DisposableTimer.DebugDuration("Syncing from database...")) + { + ProcessDatabaseInstructions(); + PruneOldInstructions(); + } + + _syncing = false; // release + } + } + + /// + /// Process instructions from the database. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void ProcessDatabaseInstructions() + { + // NOTE + // we 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that + // would be a good idea since instructions could keep getting added and then all other threads will probably get stuck from serving requests + // (depending on what the cache refreshers are doing). I think it's best we do the one time check, process them and continue, if there are + // pending requests after being processed, they'll just be processed on the next poll. + // + // FIXME not true if we're running on a background thread, assuming we can? + + var sql = new Sql().Select("*") + .From() + .Where(dto => dto.Id > _lastId) + .OrderBy(dto => dto.Id); + + var dtos = _appContext.DatabaseContext.Database.Fetch(sql); + if (dtos.Count <= 0) return; + + // only process instructions coming from a remote server, and ignore instructions coming from + // the local server as they've already been processed. We should NOT assume that the sequence of + // instructions in the database makes any sense whatsoever, because it's all async. + var localIdentity = GetLocalIdentity(); + var remoteDtos = dtos.Where(x => x.OriginIdentity != localIdentity); + + var lastId = 0; + foreach (var dto in remoteDtos) + { + try + { + var jsonArray = JsonConvert.DeserializeObject(dto.Instructions); + NotifyRefreshers(jsonArray); + lastId = dto.Id; + } + catch (JsonException ex) + { + // FIXME + // if we cannot deserialize then it's OK to skip the instructions + // but what if NotifyRefreshers throws?! + + LogHelper.Error("Could not deserialize a distributed cache instruction (\"" + dto.Instructions + "\").", ex); + } + } + + if (lastId > 0) + SaveLastSynced(lastId); + } + + /// + /// Remove old instructions from the database. + /// + private void PruneOldInstructions() + { + _appContext.DatabaseContext.Database.Delete("WHERE utcStamp < @pruneDate", + new { pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions) }); + } + + /// + /// Reads the last-synced id from file into memory. + /// + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void ReadLastSynced() + { + var path = SyncFilePath; + if (File.Exists(path) == false) return; + + var content = File.ReadAllText(path); + int last; + if (int.TryParse(content, out last)) + _lastId = last; + } + + /// + /// Updates the in-memory last-synced id and persists it to file. + /// + /// The id. + /// + /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// + private void SaveLastSynced(int id) + { + File.WriteAllText(SyncFilePath, id.ToString(CultureInfo.InvariantCulture)); + _lastId = id; + } + + /// + /// Gets the local server unique identity. + /// + /// The unique identity of the local server. + protected string GetLocalIdentity() + { + return JsonConvert.SerializeObject(new + { + machineName = NetworkHelper.MachineName, + appDomainAppId = HttpRuntime.AppDomainAppId + }); + } + + /// + /// Gets the sync file path for the local server. + /// + /// The sync file path for the local server. + private static string SyncFilePath + { + get + { + var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/DistCache/" + NetworkHelper.FileSafeMachineName); + if (Directory.Exists(tempFolder) == false) + Directory.CreateDirectory(tempFolder); + + return Path.Combine(tempFolder, HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty) + "-lastsynced.txt"); + } + } + + #endregion + + #region Notify refreshers + + private static ICacheRefresher GetRefresher(Guid id) + { + var refresher = CacheRefreshersResolver.Current.GetById(id); + if (refresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); + return refresher; + } + + private static IJsonCacheRefresher GetJsonRefresher(Guid id) + { + return GetJsonRefresher(GetRefresher(id)); + } + + private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) + { + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + return jsonRefresher; + } + + private static void NotifyRefreshers(IEnumerable jsonArray) + { + foreach (var jsonItem in jsonArray) + { + // could be a JObject in which case we can convert to a RefreshInstruction, + // otherwise it could be another JArray - in which case we'll iterate that. + var jsonObj = jsonItem as JObject; + if (jsonObj != null) + { + var instruction = jsonObj.ToObject(); + switch (instruction.RefreshType) + { + case RefreshMethodType.RefreshAll: + RefreshAll(instruction.RefresherId); + break; + case RefreshMethodType.RefreshByGuid: + RefreshByGuid(instruction.RefresherId, instruction.GuidId); + break; + case RefreshMethodType.RefreshById: + RefreshById(instruction.RefresherId, instruction.IntId); + break; + case RefreshMethodType.RefreshByIds: + RefreshByIds(instruction.RefresherId, instruction.JsonIds); + break; + case RefreshMethodType.RefreshByJson: + RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + break; + case RefreshMethodType.RemoveById: + RemoveById(instruction.RefresherId, instruction.IntId); + break; + } + + } + else + { + var jsonInnerArray = (JArray) jsonItem; + NotifyRefreshers(jsonInnerArray); // recurse + } + } + } + + private static void RefreshAll(Guid uniqueIdentifier) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.RefreshAll(); + } + + private static void RefreshByGuid(Guid uniqueIdentifier, Guid id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private static void RefreshById(Guid uniqueIdentifier, int id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Refresh(id); + } + + private static void RefreshByIds(Guid uniqueIdentifier, string jsonIds) + { + var refresher = GetRefresher(uniqueIdentifier); + foreach (var id in JsonConvert.DeserializeObject(jsonIds)) + refresher.Refresh(id); + } + + private static void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) + { + var refresher = GetJsonRefresher(uniqueIdentifier); + refresher.Refresh(jsonPayload); + } + + private static void RemoveById(Guid uniqueIdentifier, int id) + { + var refresher = GetRefresher(uniqueIdentifier); + refresher.Remove(id); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs new file mode 100644 index 0000000000..66b845f4ec --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides options to the . + /// + public class DatabaseServerMessengerOptions + { + /// + /// Initializes a new instance of the with default values. + /// + public DatabaseServerMessengerOptions() + { + DaysToRetainInstructions = 100; // 100 days + ThrottleSeconds = 5; // 5 seconds + } + + /// + /// A list of callbacks that will be invoked if the lastsynced.txt file does not exist. + /// + /// + /// These callbacks will typically be for eg rebuilding the xml cache file, or examine indexes, based on + /// the data in the database to get this particular server node up to date. + /// + public IEnumerable InitializingCallbacks { get; set; } + + /// + /// The number of days to keep instructions in the database; records older than this number will be pruned. + /// + public int DaysToRetainInstructions { get; set; } + + /// + /// The number of seconds to wait between each sync operations. + /// + public int ThrottleSeconds { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index bce812edd8..e2f400ea71 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -4,19 +4,35 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Sync { - /// - /// A registrar that stores registered server nodes in a database + /// A registrar that stores registered server nodes in the database. /// - internal class DatabaseServerRegistrar : IServerRegistrar + internal sealed class DatabaseServerRegistrar : IServerRegistrar { private readonly Lazy _registrationService; - public DatabaseServerRegistrar(Lazy registrationService) + /// + /// Gets or sets the registrar options. + /// + public DatabaseServerRegistrarOptions Options { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// The registration service. + /// Some options. + public DatabaseServerRegistrar(Lazy registrationService, DatabaseServerRegistrarOptions options) { + if (registrationService == null) throw new ArgumentNullException("registrationService"); + if (options == null) throw new ArgumentNullException("options"); + + Options = options; _registrationService = registrationService; } + /// + /// Gets the registered servers. + /// public IEnumerable Registrations { get { return _registrationService.Value.GetActiveServers(); } diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs new file mode 100644 index 0000000000..4ee7fec371 --- /dev/null +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrarOptions.cs @@ -0,0 +1,29 @@ +using System; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides options to the . + /// + public sealed class DatabaseServerRegistrarOptions + { + /// + /// Initializes a new instance of the class with default values. + /// + public DatabaseServerRegistrarOptions() + { + StaleServerTimeout = new TimeSpan(1,0,0); // 1 day + ThrottleSeconds = 30; // 30 seconds + } + + /// + /// The number of seconds to wait between each updates to the database. + /// + public int ThrottleSeconds { get; set; } + + /// + /// The time span to wait before considering a server stale, after it has last been accessed. + /// + public TimeSpan StaleServerTimeout { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs b/src/Umbraco.Core/Sync/DefaultServerMessenger.cs deleted file mode 100644 index f302dbe4d8..0000000000 --- a/src/Umbraco.Core/Sync/DefaultServerMessenger.cs +++ /dev/null @@ -1,552 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Threading; -using System.Web; -using System.Web.Script.Serialization; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using umbraco.interfaces; - -namespace Umbraco.Core.Sync -{ - /// - /// The default server messenger that uses web services to keep servers in sync - /// - internal class DefaultServerMessenger : IServerMessenger - { - private readonly Func> _getUserNamePasswordDelegate; - private volatile bool _hasResolvedDelegate = false; - private readonly object _locker = new object(); - - protected string Login { get; private set; } - protected string Password{ get; private set; } - - protected bool UseDistributedCalls { get; private set; } - - /// - /// Without a username/password all distribuion will be disabled - /// - internal DefaultServerMessenger() - { - UseDistributedCalls = false; - } - - /// - /// Distribution will be enabled based on the umbraco config setting. - /// - /// - /// - internal DefaultServerMessenger(string login, string password) - : this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) - { - } - - /// - /// Specifies the username/password and whether or not to use distributed calls - /// - /// - /// - /// - internal DefaultServerMessenger(string login, string password, bool useDistributedCalls) - { - if (login == null) throw new ArgumentNullException("login"); - if (password == null) throw new ArgumentNullException("password"); - - UseDistributedCalls = useDistributedCalls; - Login = login; - Password = password; - } - - /// - /// Allows to set a lazy delegate to resolve the username/password - /// - /// - public DefaultServerMessenger(Func> getUserNamePasswordDelegate) - { - _getUserNamePasswordDelegate = getUserNamePasswordDelegate; - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - if (jsonPayload == null) throw new ArgumentNullException("jsonPayload"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshByJson, jsonPayload: jsonPayload); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher,Func getNumericId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getNumericId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, - x => idGetter(x), - instances); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getGuidId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RefreshById, - x => idGetter(x), - instances); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - //copy local - var idGetter = getNumericId; - - MessageSeversForManyObjects(servers, refresher, MessageType.RemoveById, - x => idGetter(x), - instances); - } - - public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RemoveById, numericIds.Cast()); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshById, numericIds.Cast()); - } - - public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshById, guidIds.Cast()); - } - - public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) - { - MessageSeversForIdsOrJson(servers, refresher, MessageType.RefreshAll, Enumerable.Empty().ToArray()); - } - - private void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, Func getId, IEnumerable instances) - { - if (refresher == null) throw new ArgumentNullException("refresher"); - - LogHelper.Debug("Invoking refresher {0} on single server instance, message type {1}", - () => refresher.GetType(), - () => dispatchType); - - var stronglyTypedRefresher = refresher as ICacheRefresher; - - foreach (var instance in instances) - { - //if we are not, then just invoke the call on the cache refresher - switch (dispatchType) - { - case MessageType.RefreshAll: - refresher.RefreshAll(); - break; - case MessageType.RefreshById: - if (stronglyTypedRefresher != null) - { - stronglyTypedRefresher.Refresh(instance); - } - else - { - var id = getId(instance); - if (id is int) - { - refresher.Refresh((int)id); - } - else if (id is Guid) - { - refresher.Refresh((Guid)id); - } - else - { - throw new InvalidOperationException("The id must be either an int or a Guid"); - } - } - break; - case MessageType.RemoveById: - if (stronglyTypedRefresher != null) - { - stronglyTypedRefresher.Remove(instance); - } - else - { - var id = getId(instance); - refresher.Refresh((int)id); - } - break; - } - } - } - - /// - /// If we are instantiated with a lazy delegate to get the username/password, we'll resolve it here - /// - private void EnsureLazyUsernamePasswordDelegateResolved() - { - if (!_hasResolvedDelegate && _getUserNamePasswordDelegate != null) - { - lock (_locker) - { - if (!_hasResolvedDelegate) - { - _hasResolvedDelegate = true; //set flag - - try - { - var result = _getUserNamePasswordDelegate(); - if (result == null) - { - Login = null; - Password = null; - UseDistributedCalls = false; - } - else - { - Login = result.Item1; - Password = result.Item2; - UseDistributedCalls = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; - } - } - catch (Exception ex) - { - LogHelper.Error("Could not resolve username/password delegate, server distribution will be disabled", ex); - Login = null; - Password = null; - UseDistributedCalls = false; - } - } - } - } - } - - protected void InvokeMethodOnRefresherInstance(ICacheRefresher refresher, MessageType dispatchType, IEnumerable ids = null, string jsonPayload = null) - { - if (refresher == null) throw new ArgumentNullException("refresher"); - - LogHelper.Debug("Invoking refresher {0} on single server instance, message type {1}", - () => refresher.GetType(), - () => dispatchType); - - //if it is a refresh all we'll do it here since ids will be null or empty - if (dispatchType == MessageType.RefreshAll) - { - refresher.RefreshAll(); - } - else - { - if (ids != null) - { - foreach (var id in ids) - { - //if we are not, then just invoke the call on the cache refresher - switch (dispatchType) - { - case MessageType.RefreshById: - if (id is int) - { - refresher.Refresh((int) id); - } - else if (id is Guid) - { - refresher.Refresh((Guid) id); - } - else - { - throw new InvalidOperationException("The id must be either an int or a Guid"); - } - - break; - case MessageType.RemoveById: - refresher.Remove((int) id); - break; - } - } - } - else - { - //we can only proceed if the cache refresher is IJsonCacheRefresher! - var jsonRefresher = refresher as IJsonCacheRefresher; - if (jsonRefresher == null) - { - throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IJsonCacheRefresher)); - } - - //if we are not, then just invoke the call on the cache refresher - jsonRefresher.Refresh(jsonPayload); - } - } - } - - private void MessageSeversForManyObjects( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - Func getId, - IEnumerable instances) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - EnsureLazyUsernamePasswordDelegateResolved(); - - //Now, check if we are using Distrubuted calls. If there are no servers in the list then we - // can definitely not distribute. - if (!UseDistributedCalls || !servers.Any()) - { - //if we are not, then just invoke the call on the cache refresher - InvokeMethodOnRefresherInstance(refresher, dispatchType, getId, instances); - return; - } - - //if we are distributing calls then we'll need to do it by id - MessageSeversForIdsOrJson(servers, refresher, dispatchType, instances.Select(getId)); - } - - protected virtual void MessageSeversForIdsOrJson( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - string jsonPayload = null) - { - if (servers == null) throw new ArgumentNullException("servers"); - if (refresher == null) throw new ArgumentNullException("refresher"); - - Type arrayType; - if (!ValidateIdArray(ids, out arrayType)) - { - throw new ArgumentException("The id must be either an int or a Guid"); - } - - EnsureLazyUsernamePasswordDelegateResolved(); - - //Now, check if we are using Distrubuted calls. If there are no servers in the list then we - // can definitely not distribute. - if (!UseDistributedCalls || !servers.Any()) - { - //if we are not, then just invoke the call on the cache refresher - InvokeMethodOnRefresherInstance(refresher, dispatchType, ids, jsonPayload); - return; - } - - LogHelper.Debug( - "Performing distributed call for refresher {0}, message type: {1}, servers: {2}, ids: {3}, json: {4}", - refresher.GetType, - () => dispatchType, - () => string.Join(";", servers.Select(x => x.ToString())), - () => ids == null ? "" : string.Join(";", ids.Select(x => x.ToString())), - () => jsonPayload ?? ""); - - PerformDistributedCall(servers, refresher, dispatchType, ids, arrayType, jsonPayload); - } - - private bool ValidateIdArray(IEnumerable ids, out Type arrayType) - { - arrayType = null; - if (ids != null) - { - foreach (var id in ids) - { - if (!(id is int) && (!(id is Guid))) - return false; // - if (arrayType == null) - arrayType = id.GetType(); - if (arrayType != id.GetType()) - throw new ArgumentException("The array must contain the same type of " + arrayType); - } - } - return true; - } - - protected virtual void PerformDistributedCall( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - Type idArrayType = null, - string jsonPayload = null) - { - //We are using distributed calls, so lets make them... - try - { - - //TODO: We should try to figure out the current server's address and if it matches any of the ones - // in the ServerAddress list, then just refresh directly on this server and exclude that server address - // from the list, this will save an internal request. - - using (var cacheRefresher = new ServerSyncWebServiceClient()) - { - var asyncResultsList = new List(); - - LogStartDispatch(); - - // Go through each configured node submitting a request asynchronously - //NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! - foreach (var n in servers) - { - //set the server address - cacheRefresher.Url = n.ServerAddress; - - // Add the returned WaitHandle to the list for later checking - switch (dispatchType) - { - case MessageType.RefreshByJson: - asyncResultsList.Add( - cacheRefresher.BeginRefreshByJson( - refresher.UniqueIdentifier, jsonPayload, Login, Password, null, null)); - break; - case MessageType.RefreshAll: - asyncResultsList.Add( - cacheRefresher.BeginRefreshAll( - refresher.UniqueIdentifier, Login, Password, null, null)); - break; - case MessageType.RefreshById: - if (idArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (idArrayType == typeof(int)) - { - var serializer = new JavaScriptSerializer(); - var jsonIds = serializer.Serialize(ids.Cast().ToArray()); - //we support bulk loading of Integers - var result = cacheRefresher.BeginRefreshByIds(refresher.UniqueIdentifier, jsonIds, Login, Password, null, null); - asyncResultsList.Add(result); - } - else - { - //we don't currently support bulk loading of GUIDs (not even sure if we have any Guid ICacheRefreshers) - //so we'll just iterate - asyncResultsList.AddRange( - ids.Select(i => cacheRefresher.BeginRefreshByGuid( - refresher.UniqueIdentifier, (Guid)i, Login, Password, null, null))); - } - - break; - case MessageType.RemoveById: - //we don't currently support bulk removing so we'll iterate - asyncResultsList.AddRange( - ids.Select(i => cacheRefresher.BeginRemoveById( - refresher.UniqueIdentifier, (int)i, Login, Password, null, null))); - break; - } - } - - var waitHandlesList = asyncResultsList.Select(x => x.AsyncWaitHandle).ToArray(); - - var errorCount = 0; - - //Wait for all requests to complete - WaitHandle.WaitAll(waitHandlesList.ToArray()); - - foreach (var t in asyncResultsList) - { - //var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); - - try - { - // Find out if the call succeeded - switch (dispatchType) - { - case MessageType.RefreshByJson: - cacheRefresher.EndRefreshByJson(t); - break; - case MessageType.RefreshAll: - cacheRefresher.EndRefreshAll(t); - break; - case MessageType.RefreshById: - if (idArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (idArrayType == typeof(int)) - { - cacheRefresher.EndRefreshById(t); - } - else - { - cacheRefresher.EndRefreshByGuid(t); - } - break; - case MessageType.RemoveById: - cacheRefresher.EndRemoveById(t); - break; - } - } - catch (WebException ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - catch (Exception ex) - { - LogDispatchNodeError(ex); - - errorCount++; - } - } - - LogDispatchBatchResult(errorCount); - } - } - catch (Exception ee) - { - LogDispatchBatchError(ee); - } - } - - private void LogDispatchBatchError(Exception ee) - { - LogHelper.Error("Error refreshing distributed list", ee); - } - - private void LogDispatchBatchResult(int errorCount) - { - LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); - } - - private void LogDispatchNodeError(Exception ex) - { - LogHelper.Error("Error refreshing a node in the distributed list", ex); - } - - private void LogDispatchNodeError(WebException ex) - { - string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; - LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); - } - - private void LogStartDispatch() - { - LogHelper.Info("Submitting calls to distributed servers"); - } - - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerAddress.cs b/src/Umbraco.Core/Sync/IServerAddress.cs index 0483af1800..84a3563c60 100644 --- a/src/Umbraco.Core/Sync/IServerAddress.cs +++ b/src/Umbraco.Core/Sync/IServerAddress.cs @@ -3,10 +3,13 @@ namespace Umbraco.Core.Sync { /// - /// An interface exposing a server address to use for server syncing + /// Provides the address of a server. /// public interface IServerAddress { + /// + /// Gets the server address. + /// string ServerAddress { get; } //TODO : Should probably add things like port, protocol, server name, app id diff --git a/src/Umbraco.Core/Sync/IServerMessenger.cs b/src/Umbraco.Core/Sync/IServerMessenger.cs index 568fb86026..100638c202 100644 --- a/src/Umbraco.Core/Sync/IServerMessenger.cs +++ b/src/Umbraco.Core/Sync/IServerMessenger.cs @@ -5,81 +5,97 @@ using umbraco.interfaces; namespace Umbraco.Core.Sync { /// - /// Defines a server messenger for server sync and distrubuted cache + /// Broadcasts distributed cache notifications to all servers of a load balanced environment. /// + /// Also ensures that the notification is processed on the local environment. public interface IServerMessenger { + // TODO + // everything we do "by JSON" means that data is serialized then deserialized on the local server + // we should stop using this, and instead use Notify() with an actual object that can be passed + // around locally, and serialized for remote messaging - but that would break backward compat ;-( + // + // and then ServerMessengerBase must be able to handle Notify(), and all messengers too + // and then ICacheRefresher (or INotifiableCacheRefresher?) must be able to handle it too + // + // >> v8 /// - /// Performs a refresh and sends along the JSON payload to each server + /// Notifies the distributed cache, for a specified . /// - /// - /// - /// - /// A pre-formatted custom json payload to be sent to the servers, the cache refresher will deserialize and use to refresh cache - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The notification content. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload); + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The servers that compose the load balanced environment. + ///// The ICacheRefresher. + ///// The notification content. + ///// A custom Json serializer. + //void Notify(IEnumerable servers, ICacheRefresher refresher, object payload, Func serializer = null); + /// - /// Performs a sync against all instance objects + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// The servers to sync against - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the invalidated items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); - + /// - /// Performs a sync against all instance objects + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// The servers to sync against - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the invalidated items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances); /// - /// Removes the cache for the specified items + /// Notifies all servers of specified items removal, for a specified . /// - /// - /// - /// - /// A delegate to return the Id for each instance to be used to sync to other servers - /// + /// The type of the removed items. + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances); /// - /// Removes the cache for the specified items + /// Notifies all servers of specified items removal, for a specified . /// - /// - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the removed items. void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); /// - /// Performs a sync against all Ids + /// Notifies all servers of specified items invalidation, for a specified . /// - /// The servers to sync against - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds); - + /// - /// Performs a sync against all Ids + /// Notifies all servers of specified items invalidation, for a specified . /// - /// The servers to sync against - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. + /// The unique identifiers of the invalidated items. void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds); /// - /// Performs entire cache refresh for a specified refresher + /// Notifies all servers of a global invalidation for a specified . /// - /// - /// + /// The servers that compose the load balanced environment. + /// The ICacheRefresher. void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher); } - } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/IServerRegistrar.cs b/src/Umbraco.Core/Sync/IServerRegistrar.cs index 46e0c268f1..5f63440859 100644 --- a/src/Umbraco.Core/Sync/IServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/IServerRegistrar.cs @@ -3,10 +3,13 @@ namespace Umbraco.Core.Sync { /// - /// An interface to expose a list of server registrations for server syncing + /// Provides server registrations to the distributed cache. /// public interface IServerRegistrar { + /// + /// Gets the server registrations. + /// IEnumerable Registrations { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/MessageType.cs b/src/Umbraco.Core/Sync/MessageType.cs index aa8733be33..80e9bcae76 100644 --- a/src/Umbraco.Core/Sync/MessageType.cs +++ b/src/Umbraco.Core/Sync/MessageType.cs @@ -1,7 +1,7 @@ namespace Umbraco.Core.Sync { /// - /// The message type to be used for syncing across servers + /// The message type to be used for syncing across servers. /// public enum MessageType { diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index 867266085b..a950b9bf78 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -1,46 +1,137 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using umbraco.interfaces; namespace Umbraco.Core.Sync { [Serializable] public class RefreshInstruction { - public RefreshMethodType RefreshType { get; set; } - public Guid RefresherId { get; set; } - public Guid GuidId { get; set; } - public int IntId { get; set; } - public string JsonIds { get; set; } - public string JsonPayload { get; set; } + // NOTE + // that class should be refactored + // but at the moment it is exposed in CacheRefresher webservice + // so for the time being we keep it as-is for backward compatibility reasons - [Serializable] - public enum RefreshMethodType + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) { - RefreshAll, - RefreshByGuid, - RefreshById, - RefreshByIds, - RefreshByJson, - RemoveById + RefresherId = refresher.UniqueIdentifier; + RefreshType = refreshType; } + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, Guid guidId) + : this(refresher, refreshType) + { + GuidId = guidId; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, int intId) + : this(refresher, refreshType) + { + IntId = intId; + } + + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType, string json) + : this(refresher, refreshType) + { + if (refreshType == RefreshMethodType.RefreshByJson) + JsonPayload = json; + else + JsonIds = json; + } + + public static IEnumerable GetInstructions( + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids, + Type idType, + string json) + { + switch (messageType) + { + case MessageType.RefreshAll: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshAll) }; + + case MessageType.RefreshByJson: + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByJson, json) }; + + case MessageType.RefreshById: + if (idType == null) + throw new InvalidOperationException("Cannot refresh by id if idType is null."); + if (idType == typeof (int)) // bulk of ints is supported + return new[] { new RefreshInstruction(refresher, RefreshMethodType.RefreshByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + // else must be guids, bulk of guids is not supported, iterate + return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RefreshByGuid, (Guid) x)); + + case MessageType.RemoveById: + if (idType == null) + throw new InvalidOperationException("Cannot remove by id if idType is null."); + // must be ints, bulk-remove is not supported, iterate + return ids.Select(x => new RefreshInstruction(refresher, RefreshMethodType.RemoveById, (int) x)); + //return new[] { new RefreshInstruction(refresher, RefreshMethodType.RemoveByIds, JsonConvert.SerializeObject(ids.Cast().ToArray())) }; + + default: + //case MessageType.RefreshByInstance: + //case MessageType.RemoveByInstance: + throw new ArgumentOutOfRangeException("messageType"); + } + } + + /// + /// Gets or sets the refresh action type. + /// + public RefreshMethodType RefreshType { get; set; } + + /// + /// Gets or sets the refresher unique identifier. + /// + public Guid RefresherId { get; set; } + + /// + /// Gets or sets the Guid data value. + /// + public Guid GuidId { get; set; } + + /// + /// Gets or sets the int data value. + /// + public int IntId { get; set; } + + /// + /// Gets or sets the ids data value. + /// + public string JsonIds { get; set; } + + /// + /// Gets or sets the payload data value. + /// + public string JsonPayload { get; set; } + protected bool Equals(RefreshInstruction other) { - return RefreshType == other.RefreshType && RefresherId.Equals(other.RefresherId) && GuidId.Equals(other.GuidId) && IntId == other.IntId && string.Equals(JsonIds, other.JsonIds) && string.Equals(JsonPayload, other.JsonPayload); + return RefreshType == other.RefreshType + && RefresherId.Equals(other.RefresherId) + && GuidId.Equals(other.GuidId) + && IntId == other.IntId + && string.Equals(JsonIds, other.JsonIds) + && string.Equals(JsonPayload, other.JsonPayload); } - public override bool Equals(object obj) + public override bool Equals(object other) { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; - return Equals((RefreshInstruction) obj); + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + if (other.GetType() != this.GetType()) return false; + return Equals((RefreshInstruction) other); } public override int GetHashCode() { unchecked { - int hashCode = (int) RefreshType; + var hashCode = (int) RefreshType; hashCode = (hashCode*397) ^ RefresherId.GetHashCode(); hashCode = (hashCode*397) ^ GuidId.GetHashCode(); hashCode = (hashCode*397) ^ IntId; @@ -57,7 +148,7 @@ namespace Umbraco.Core.Sync public static bool operator !=(RefreshInstruction left, RefreshInstruction right) { - return !Equals(left, right); + return Equals(left, right) == false; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs new file mode 100644 index 0000000000..12922d6dab --- /dev/null +++ b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + public class RefreshInstructionEnvelope + { + public RefreshInstructionEnvelope(IEnumerable servers, ICacheRefresher refresher, IEnumerable instructions) + { + Servers = servers; + Refresher = refresher; + Instructions = instructions; + } + + public IEnumerable Servers { get; set; } + public ICacheRefresher Refresher { get; set; } + public IEnumerable Instructions { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/RefreshMethodType.cs b/src/Umbraco.Core/Sync/RefreshMethodType.cs new file mode 100644 index 0000000000..4f6ad64716 --- /dev/null +++ b/src/Umbraco.Core/Sync/RefreshMethodType.cs @@ -0,0 +1,44 @@ +using System; + +namespace Umbraco.Core.Sync +{ + /// + /// Describes refresh action type. + /// + [Serializable] + public enum RefreshMethodType + { + // NOTE + // that enum should get merged somehow with MessageType and renamed somehow + // but at the moment it is exposed in CacheRefresher webservice through RefreshInstruction + // so for the time being we keep it as-is for backward compatibility reasons + + RefreshAll, + RefreshByGuid, + RefreshById, + RefreshByIds, + RefreshByJson, + RemoveById, + + // would adding values break backward compatibility? + //RemoveByIds + + // these are MessageType values + // note that AnythingByInstance are local messages and cannot be distributed + /* + RefreshAll, + RefreshById, + RefreshByJson, + RemoveById, + RefreshByInstance, + RemoveByInstance + */ + + // NOTE + // in the future we want + // RefreshAll + // RefreshById / ByInstance (support enumeration of int or guid) + // RemoveById / ByInstance (support enumeration of int or guid) + // Notify (for everything JSON) + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs new file mode 100644 index 0000000000..fed29b85a8 --- /dev/null +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -0,0 +1,317 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// Provides a base class for all implementations. + /// + public abstract class ServerMessengerBase : IServerMessenger + { + protected bool DistributedEnabled { get; set; } + + protected ServerMessengerBase(bool distributedEnabled) + { + DistributedEnabled = distributedEnabled; + } + + /// + /// Determines whether to make distributed calls when messaging a cache refresher. + /// + /// The registered servers. + /// The cache refresher. + /// The message type. + /// true if distributed calls are required; otherwise, false, all we have is the local server. + protected virtual bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType messageType) + { + return DistributedEnabled && servers.Any(); + } + + // ensures that all items in the enumerable are of the same type, either int or Guid. + protected static bool GetArrayType(IEnumerable ids, out Type arrayType) + { + arrayType = null; + if (ids == null) return true; + + foreach (var id in ids) + { + // only int and Guid are supported + if ((id is int) == false && ((id is Guid) == false)) + return false; + // initialize with first item + if (arrayType == null) + arrayType = id.GetType(); + // check remaining items + if (arrayType != id.GetType()) + return false; + } + + return true; + } + + #region IServerMessenger + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, string jsonPayload) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (jsonPayload == null) throw new ArgumentNullException("jsonPayload"); + + Deliver(servers, refresher, MessageType.RefreshByJson, json: jsonPayload); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getNumericId(x); + Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, Func getGuidId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getGuidId == null) throw new ArgumentNullException("getGuidId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getGuidId(x); + Deliver(servers, refresher, MessageType.RefreshByInstance, getId, instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, Func getNumericId, params T[] instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (getNumericId == null) throw new ArgumentNullException("getNumericId"); + if (instances == null || instances.Length == 0) return; + + Func getId = x => getNumericId(x); + Deliver(servers, refresher, MessageType.RemoveByInstance, getId, instances); + } + + public void PerformRemove(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (numericIds == null || numericIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RemoveById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params int[] numericIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (numericIds == null || numericIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RefreshById, numericIds.Cast()); + } + + public void PerformRefresh(IEnumerable servers, ICacheRefresher refresher, params Guid[] guidIds) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + if (guidIds == null || guidIds.Length == 0) return; + + Deliver(servers, refresher, MessageType.RefreshById, guidIds.Cast()); + } + + public void PerformRefreshAll(IEnumerable servers, ICacheRefresher refresher) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + Deliver(servers, refresher, MessageType.RefreshAll); + } + + //public void PerformNotify(IEnumerable servers, ICacheRefresher refresher, object payload) + //{ + // if (servers == null) throw new ArgumentNullException("servers"); + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // Deliver(servers, refresher, payload); + //} + + #endregion + + #region Deliver + + protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + refresher.GetType, + () => messageType); + + switch (messageType) + { + case MessageType.RefreshAll: + refresher.RefreshAll(); + break; + + case MessageType.RefreshById: + if (ids != null) + foreach (var id in ids) + { + if (id is int) + refresher.Refresh((int) id); + else if (id is Guid) + refresher.Refresh((Guid) id); + else + throw new InvalidOperationException("The id must be either an int or a Guid."); + } + break; + + case MessageType.RefreshByJson: + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not of type " + typeof(IJsonCacheRefresher)); + jsonRefresher.Refresh(json); + break; + + case MessageType.RemoveById: + if (ids != null) + foreach (var id in ids) + { + if (id is int) + refresher.Remove((int) id); + else + throw new InvalidOperationException("The id must be an int."); + } + break; + + default: + //case MessageType.RefreshByInstance: + //case MessageType.RemoveByInstance: + throw new NotSupportedException("Invalid message type " + messageType); + } + } + + protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) + { + if (refresher == null) throw new ArgumentNullException("refresher"); + + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + refresher.GetType, + () => messageType); + + var typedRefresher = refresher as ICacheRefresher; + + switch (messageType) + { + case MessageType.RefreshAll: + refresher.RefreshAll(); + break; + + case MessageType.RefreshByInstance: + if (typedRefresher == null) + throw new InvalidOperationException("The refresher must be a typed refresher."); + foreach (var instance in instances) + typedRefresher.Refresh(instance); + break; + + case MessageType.RemoveByInstance: + if (typedRefresher == null) + throw new InvalidOperationException("The cache refresher " + refresher.GetType() + " is not a typed refresher."); + foreach (var instance in instances) + typedRefresher.Remove(instance); + break; + + default: + //case MessageType.RefreshById: + //case MessageType.RemoveById: + //case MessageType.RefreshByJson: + throw new NotSupportedException("Invalid message type " + messageType); + } + } + + //protected void DeliverLocal(ICacheRefresher refresher, object payload) + //{ + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // LogHelper.Debug("Invoking refresher {0} on local server for message type Notify", + // () => refresher.GetType()); + + // refresher.Notify(payload); + //} + + protected abstract void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null); + + //protected abstract void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, object payload); + + protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + var serversA = servers.ToArray(); + var idsA = ids == null ? null : ids.ToArray(); + + // deliver local + DeliverLocal(refresher, messageType, idsA, json); + + // distribute? + if (RequiresDistributed(serversA, refresher, messageType) == false) + return; + + // deliver remote + DeliverRemote(serversA, refresher, messageType, idsA, json); + } + + protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) + { + if (servers == null) throw new ArgumentNullException("servers"); + if (refresher == null) throw new ArgumentNullException("refresher"); + + var serversA = servers.ToArray(); + var instancesA = instances.ToArray(); + + // deliver local + DeliverLocal(refresher, messageType, getId, instancesA); + + // distribute? + if (RequiresDistributed(serversA, refresher, messageType) == false) + return; + + // deliver remote + + // map ByInstance to ById as there's no remote instances + if (messageType == MessageType.RefreshByInstance) messageType = MessageType.RefreshById; + if (messageType == MessageType.RemoveByInstance) messageType = MessageType.RemoveById; + + // convert instances to identifiers + var idsA = instancesA.Select(getId).ToArray(); + + DeliverRemote(serversA, refresher, messageType, idsA); + } + + //protected virtual void Deliver(IEnumerable servers, ICacheRefresher refresher, object payload) + //{ + // if (servers == null) throw new ArgumentNullException("servers"); + // if (refresher == null) throw new ArgumentNullException("refresher"); + + // var serversA = servers.ToArray(); + + // // deliver local + // DeliverLocal(refresher, payload); + + // // distribute? + // if (RequiresDistributed(serversA, refresher, messageType) == false) + // return; + + // // deliver remote + // DeliverRemote(serversA, refresher, payload); + //} + + #endregion + } +} diff --git a/src/Umbraco.Core/Sync/ServerMessengerResolver.cs b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs index 549f3520e0..b508d18f16 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerResolver.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerResolver.cs @@ -3,24 +3,31 @@ namespace Umbraco.Core.Sync { /// - /// A resolver to return the currently registered IServerMessenger object + /// Resolves the IServerMessenger object. /// public sealed class ServerMessengerResolver : SingleObjectResolverBase { + /// + /// Initializes a new instance of the class with a messenger. + /// + /// An instance of a messenger. + /// The resolver is created by the CoreBootManager and thus the constructor remains internal. internal ServerMessengerResolver(IServerMessenger factory) : base(factory) - { - } + { } /// - /// Can be used at runtime to set a custom IServerMessenger at app startup + /// Sets the messenger. /// - /// + /// The messenger. public void SetServerMessenger(IServerMessenger serverMessenger) { Value = serverMessenger; } + /// + /// Gets the messenger. + /// public IServerMessenger Messenger { get { return Value; } diff --git a/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs index 196c6fb74c..595dfc0ceb 100644 --- a/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs +++ b/src/Umbraco.Core/Sync/ServerRegistrarResolver.cs @@ -3,25 +3,32 @@ namespace Umbraco.Core.Sync { /// - /// The resolver to return the currently registered IServerRegistrar object + /// Resolves the IServerRegistrar object. /// public sealed class ServerRegistrarResolver : SingleObjectResolverBase { - + /// + /// Initializes a new instance of the class with a registrar. + /// + /// An instance of a registrar. + /// The resolver is created by the CoreBootManager and thus the constructor remains internal. internal ServerRegistrarResolver(IServerRegistrar factory) : base(factory) - { - } + { } /// - /// Can be used at runtime to set a custom IServerRegistrar at app startup + /// Sets the registrar. /// - /// + /// The registrar. + /// For developers, at application startup. public void SetServerRegistrar(IServerRegistrar serverRegistrar) { Value = serverRegistrar; } + /// + /// Gets the registrar. + /// public IServerRegistrar Registrar { get { return Value; } diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs new file mode 100644 index 0000000000..c4b3c03d2f --- /dev/null +++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading; +using Newtonsoft.Json; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using umbraco.interfaces; + +namespace Umbraco.Core.Sync +{ + /// + /// An that works by messaging servers via web services. + /// + // + // this messenger sends ALL instructions to ALL servers, including the local server. + // the CacheRefresher web service will run ALL instructions, so there may be duplicated, + // except for "bulk" refresh, where it excludes those coming from the local server + // + // TODO see Message() method: stop sending to local server! + // just need to figure out WebServerUtility permissions issues, if any + // + internal class WebServiceServerMessenger : ServerMessengerBase + { + private readonly Func> _getLoginAndPassword; + private volatile bool _hasLoginAndPassword; + private readonly object _locker = new object(); + + protected string Login { get; private set; } + protected string Password{ get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// Distribution is disabled. + internal WebServiceServerMessenger() + : base(false) + { } + + /// + /// Initializes a new instance of the class with a login and a password. + /// + /// The login. + /// The password. + /// Distribution will be enabled based on the umbraco config setting. + internal WebServiceServerMessenger(string login, string password) + : this(login, password, UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled) + { + } + + /// + /// Initializes a new instance of the class with a login and a password + /// and a value indicating whether distribution is enabled. + /// + /// The login. + /// The password. + /// A value indicating whether distribution is enabled. + internal WebServiceServerMessenger(string login, string password, bool distributedEnabled) + : base(distributedEnabled) + { + if (login == null) throw new ArgumentNullException("login"); + if (password == null) throw new ArgumentNullException("password"); + + Login = login; + Password = password; + } + + /// + /// Initializes a new instance of the with a function providing + /// a login and a password. + /// + /// A function providing a login and a password. + /// Distribution will be enabled based on the umbraco config setting. + public WebServiceServerMessenger(Func> getLoginAndPassword) + : base(false) // value will be overriden by EnsureUserAndPassword + { + _getLoginAndPassword = getLoginAndPassword; + } + + // lazy-get the login, password, and distributed setting + protected void EnsureLoginAndPassword() + { + if (_hasLoginAndPassword || _getLoginAndPassword == null) return; + + lock (_locker) + { + if (_hasLoginAndPassword) return; + _hasLoginAndPassword = true; + + try + { + var result = _getLoginAndPassword(); + if (result == null) + { + Login = null; + Password = null; + DistributedEnabled = false; + } + else + { + Login = result.Item1; + Password = result.Item2; + DistributedEnabled = UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled; + } + } + catch (Exception ex) + { + LogHelper.Error("Could not resolve username/password delegate, server distribution will be disabled", ex); + Login = null; + Password = null; + DistributedEnabled = false; + } + } + } + + // this exists only for legacy reasons - we should just pass the server identity un-hashed + public static string GetCurrentServerHash() + { + if (SystemUtilities.GetCurrentTrustLevel() != System.Web.AspNetHostingPermissionLevel.Unrestricted) + throw new NotSupportedException("FullTrust ASP.NET permission level is required."); + return GetServerHash(NetworkHelper.MachineName, System.Web.HttpRuntime.AppDomainAppId); + } + + public static string GetServerHash(string machineName, string appDomainAppId) + { + var hasher = new HashCodeCombiner(); + hasher.AddCaseInsensitiveString(NetworkHelper.MachineName); + hasher.AddCaseInsensitiveString(System.Web.HttpRuntime.AppDomainAppId); + return hasher.GetCombinedHashCode(); + } + + protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType messageType) + { + EnsureLoginAndPassword(); + return base.RequiresDistributed(servers, refresher, messageType); + } + + protected override void DeliverRemote(IEnumerable servers, ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) + { + var idsA = ids == null ? null : ids.ToArray(); + + Type arrayType; + if (GetArrayType(idsA, out arrayType) == false) + throw new ArgumentException("All items must be of the same type, either int or Guid.", "ids"); + + Message(servers, refresher, messageType, idsA, arrayType, json); + } + + protected virtual void Message( + IEnumerable servers, + ICacheRefresher refresher, + MessageType messageType, + IEnumerable ids = null, + Type idArrayType = null, + string jsonPayload = null) + { + LogHelper.Debug( + "Performing distributed call for {0}/{1} on servers ({2}), ids: {3}, json: {4}", + refresher.GetType, + () => messageType, + () => string.Join(";", servers.Select(x => x.ToString())), + () => ids == null ? "" : string.Join(";", ids.Select(x => x.ToString())), + () => jsonPayload ?? ""); + + try + { + // NOTE: we are messaging ALL servers including the local server + // at the moment, the web service, + // for bulk (batched) checks the origin and does NOT process the instructions again + // for anything else, processes the instructions again (but we don't use this anymore, batched is the default) + // TODO: see WebServerHelper, could remove local server from the list of servers + + // the default server messenger uses http requests + using (var client = new ServerSyncWebServiceClient()) + { + var asyncResults = new List(); + + LogStartDispatch(); + + // go through each configured node submitting a request asynchronously + // NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! + foreach (var n in servers) + { + // set the server address + client.Url = n.ServerAddress; + + // add the returned WaitHandle to the list for later checking + switch (messageType) + { + case MessageType.RefreshByJson: + asyncResults.Add(client.BeginRefreshByJson(refresher.UniqueIdentifier, jsonPayload, Login, Password, null, null)); + break; + + case MessageType.RefreshAll: + asyncResults.Add(client.BeginRefreshAll(refresher.UniqueIdentifier, Login, Password, null, null)); + break; + + case MessageType.RefreshById: + if (idArrayType == null) + throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null."); + + if (idArrayType == typeof(int)) + { + // bulk of ints is supported + var json = JsonConvert.SerializeObject(ids.Cast().ToArray()); + var result = client.BeginRefreshByIds(refresher.UniqueIdentifier, json, Login, Password, null, null); + asyncResults.Add(result); + } + else // must be guids + { + // bulk of guids is not supported, iterate + asyncResults.AddRange(ids.Select(i => + client.BeginRefreshByGuid(refresher.UniqueIdentifier, (Guid)i, Login, Password, null, null))); + } + + break; + case MessageType.RemoveById: + if (idArrayType == null) + throw new InvalidOperationException("Cannot remove by id if the idArrayType is null."); + + // must be ints + asyncResults.AddRange(ids.Select(i => + client.BeginRemoveById(refresher.UniqueIdentifier, (int)i, Login, Password, null, null))); + break; + } + } + + // wait for all requests to complete + var waitHandles = asyncResults.Select(x => x.AsyncWaitHandle); + WaitHandle.WaitAll(waitHandles.ToArray()); + + // handle results + var errorCount = 0; + foreach (var asyncResult in asyncResults) + { + try + { + switch (messageType) + { + case MessageType.RefreshByJson: + client.EndRefreshByJson(asyncResult); + break; + + case MessageType.RefreshAll: + client.EndRefreshAll(asyncResult); + break; + + case MessageType.RefreshById: + if (idArrayType == typeof(int)) + client.EndRefreshById(asyncResult); + else + client.EndRefreshByGuid(asyncResult); + break; + + case MessageType.RemoveById: + client.EndRemoveById(asyncResult); + break; + } + } + catch (WebException ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + catch (Exception ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + } + + LogDispatchBatchResult(errorCount); + } + } + catch (Exception ee) + { + LogDispatchBatchError(ee); + } + } + + protected virtual void Message(IEnumerable envelopes) + { + var envelopesA = envelopes.ToArray(); + var servers = envelopesA.SelectMany(x => x.Servers).Distinct(); + + try + { + // NOTE: we are messaging ALL servers including the local server + // at the moment, the web service, + // for bulk (batched) checks the origin and does NOT process the instructions again + // for anything else, processes the instructions again (but we don't use this anymore, batched is the default) + // TODO: see WebServerHelper, could remove local server from the list of servers + + using (var client = new ServerSyncWebServiceClient()) + { + var asyncResults = new List(); + + LogStartDispatch(); + + // go through each configured node submitting a request asynchronously + // NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! + foreach (var server in servers) + { + // set the server address + client.Url = server.ServerAddress; + + var serverInstructions = envelopesA + .Where(x => x.Servers.Contains(server)) + .SelectMany(x => x.Instructions) + .Distinct() // only execute distinct instructions - no sense in running the same one. + .ToArray(); + + asyncResults.Add( + client.BeginBulkRefresh( + serverInstructions, + GetCurrentServerHash(), + Login, Password, null, null)); + } + + // wait for all requests to complete + var waitHandles = asyncResults.Select(x => x.AsyncWaitHandle).ToArray(); + WaitHandle.WaitAll(waitHandles.ToArray()); + + // handle results + var errorCount = 0; + foreach (var asyncResult in asyncResults) + { + try + { + client.EndBulkRefresh(asyncResult); + } + catch (WebException ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + catch (Exception ex) + { + LogDispatchNodeError(ex); + errorCount++; + } + } + LogDispatchBatchResult(errorCount); + } + } + catch (Exception ee) + { + LogDispatchBatchError(ee); + } + } + + #region Logging + + private static void LogDispatchBatchError(Exception ee) + { + LogHelper.Error("Error refreshing distributed list", ee); + } + + private static void LogDispatchBatchResult(int errorCount) + { + LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); + } + + private static void LogDispatchNodeError(Exception ex) + { + LogHelper.Error("Error refreshing a node in the distributed list", ex); + } + + private static void LogDispatchNodeError(WebException ex) + { + string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; + LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); + } + + private static void LogStartDispatch() + { + LogHelper.Info("Submitting calls to distributed servers"); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 947401b799..486772360a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -385,9 +385,11 @@ + + @@ -457,6 +459,7 @@ + @@ -1174,15 +1177,23 @@ + + + + + + + + - + Component diff --git a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs index c5be44dcdd..690ad80525 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Core.Sync; using umbraco.presentation.webservices; namespace Umbraco.Tests.Cache @@ -13,8 +14,10 @@ namespace Umbraco.Tests.Cache [TestCase("fffffff28449cf3", "123456", "testmachine", true)] public void Continue_Refreshing_For_Request(string hash, string appDomainAppId, string machineName, bool expected) { - var refresher = new CacheRefresher(); - Assert.AreEqual(expected, refresher.ContinueRefreshingForRequest(hash, appDomainAppId, machineName)); + if (expected) + Assert.AreEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); + else + Assert.AreNotEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); } } diff --git a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs index 9a5b994522..463657c4bd 100644 --- a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs +++ b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs @@ -223,11 +223,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } @@ -252,11 +252,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } db.OpenSharedConnection(); @@ -283,11 +283,11 @@ namespace Umbraco.Tests.Persistence { servers.Add(new ServerRegistrationDto { - Address = "address" + i, - ComputerName = "computer" + i, + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, DateRegistered = DateTime.Now, IsActive = true, - LastNotified = DateTime.Now + DateAccessed = DateTime.Now }); } db.OpenSharedConnection(); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index 9433c9a2b6..c32e214177 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -32,7 +32,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Cannot_Add_Duplicate_Computer_Names() + public void Cannot_Add_Duplicate_Server_Identities() { // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -50,7 +50,7 @@ namespace Umbraco.Tests.Persistence.Repositories } [Test] - public void Cannot_Update_To_Duplicate_Computer_Names() + public void Cannot_Update_To_Duplicate_Server_Identities() { // Arrange var provider = new PetaPocoUnitOfWorkProvider(Logger); @@ -60,7 +60,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepositor(unitOfWork)) { var server = repository.Get(1); - server.ComputerName = "COMPUTER2"; + server.ServerIdentity = "COMPUTER2"; repository.AddOrUpdate(server); Assert.Throws(unitOfWork.Commit); } @@ -128,7 +128,7 @@ namespace Umbraco.Tests.Persistence.Repositories using (var repository = CreateRepositor(unitOfWork)) { // Act - var query = Query.Builder.Where(x => x.ComputerName.ToUpper() == "COMPUTER3"); + var query = Query.Builder.Where(x => x.ServerIdentity.ToUpper() == "COMPUTER3"); var result = repository.GetByQuery(query); // Assert diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs new file mode 100644 index 0000000000..f98138c7f3 --- /dev/null +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Sync; +using Umbraco.Web.Routing; + +namespace Umbraco.Web +{ + public class BatchedDatabaseServerMessenger : Core.Sync.BatchedDatabaseServerMessenger + { + public BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options) + : base(appContext, enableDistCalls, options, GetBatch) + { + UmbracoApplicationBase.ApplicationStarted += Application_Started; + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + UmbracoModule.RouteAttempt += UmbracoModule_RouteAttempt; + } + + private void Application_Started(object sender, EventArgs eventArgs) + { + if (ApplicationContext.IsConfigured == false + || ApplicationContext.DatabaseContext.IsDatabaseConfigured == false + || ApplicationContext.DatabaseContext.CanConnect == false) + + LogHelper.Warn("The app is not configured or cannot connect to the database, this server cannot be initialized with " + + typeof(BatchedDatabaseServerMessenger) + ", distributed calls will not be enabled for this server"); + + // because .ApplicationStarted triggers only once, this is thread-safe + Boot(); + } + + private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs e) + { + switch (e.Outcome) + { + case EnsureRoutableOutcome.IsRoutable: + Sync(); + break; + case EnsureRoutableOutcome.NotDocumentRequest: + //so it's not a document request, we'll check if it's a back office request + if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) + { + //it's a back office request, we should sync! + Sync(); + } + break; + //case EnsureRoutableOutcome.NotReady: + //case EnsureRoutableOutcome.NotConfigured: + //case EnsureRoutableOutcome.NoContent: + //default: + // break; + } + } + + private void UmbracoModule_EndRequest(object sender, EventArgs e) + { + // will clear the batch - will remain in HttpContext though - that's ok + FlushBatch(); + } + + private static ICollection GetBatch(bool ensure) + { + var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; + if (httpContext == null) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + + var key = typeof (BatchedDatabaseServerMessenger).Name; + + // no thread-safety here because it'll run in only 1 thread (request) at a time + var batch = (ICollection)httpContext.Items[key]; + if (batch == null && ensure) + httpContext.Items[key] = batch = new List(); + return batch; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/BatchedServerMessenger.cs b/src/Umbraco.Web/BatchedServerMessenger.cs deleted file mode 100644 index 65de928e56..0000000000 --- a/src/Umbraco.Web/BatchedServerMessenger.cs +++ /dev/null @@ -1,326 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Script.Serialization; -using System.Web.UI.WebControls; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Logging; -using Umbraco.Core.Sync; -using umbraco.interfaces; - -namespace Umbraco.Web -{ - internal class BatchedServerMessenger : DefaultServerMessenger - { - internal BatchedServerMessenger() - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - internal BatchedServerMessenger(string login, string password) : base(login, password) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - internal BatchedServerMessenger(string login, string password, bool useDistributedCalls) : base(login, password, useDistributedCalls) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - public BatchedServerMessenger(Func> getUserNamePasswordDelegate) : base(getUserNamePasswordDelegate) - { - UmbracoModule.EndRequest += UmbracoModule_EndRequest; - } - - void UmbracoModule_EndRequest(object sender, EventArgs e) - { - if (HttpContext.Current == null) - { - return; - } - - var items = HttpContext.Current.Items[typeof(BatchedServerMessenger).Name] as List; - if (items != null) - { - var copied = new Message[items.Count]; - items.CopyTo(copied); - //now set to null so it get's cleaned up on this request - HttpContext.Current.Items[typeof (BatchedServerMessenger).Name] = null; - - SendMessages(copied); - } - } - - private class Message - { - public IEnumerable Servers { get; set; } - public ICacheRefresher Refresher { get; set; } - public MessageType DispatchType { get; set; } - public IEnumerable Ids { get; set; } - public Type IdArrayType { get; set; } - public string JsonPayload { get; set; } - } - - /// - /// We need to check if distributed calls are enabled, if they are we also want to make sure - /// that the current server's cache is updated internally in real time instead of at the end of - /// the call. This is because things like the URL cache, etc... might need to be updated during - /// the request that is making these calls. - /// - /// - /// - /// - /// - /// - /// - /// See: http://issues.umbraco.org/issue/U4-2633#comment=67-15604 - /// - protected override void MessageSeversForIdsOrJson(IEnumerable servers, ICacheRefresher refresher, MessageType dispatchType, IEnumerable ids = null, string jsonPayload = null) - { - //do all the normal stuff - base.MessageSeversForIdsOrJson(servers, refresher, dispatchType, ids, jsonPayload); - - //Now, check if we are using Distrubuted calls - if (UseDistributedCalls && servers.Any()) - { - //invoke on the current server - we will basically be double cache refreshing for the calling - // server but that just needs to be done currently, see the link above for details. - InvokeMethodOnRefresherInstance(refresher, dispatchType, ids, jsonPayload); - } - } - - /// - /// This adds the call to batched list - /// - /// - /// - /// - /// - /// - /// - protected override void PerformDistributedCall( - IEnumerable servers, - ICacheRefresher refresher, - MessageType dispatchType, - IEnumerable ids = null, - Type idArrayType = null, - string jsonPayload = null) - { - - //NOTE: we use UmbracoContext instead of HttpContext.Current because when some web methods run async, the - // HttpContext.Current is null but the UmbracoContext.Current won't be since we manually assign it. - if (UmbracoContext.Current == null || UmbracoContext.Current.HttpContext == null) - { - throw new NotSupportedException("This messenger cannot execute without a valid/current UmbracoContext with an HttpContext assigned"); - } - - if (UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name] == null) - { - UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name] = new List(); - } - var list = (List)UmbracoContext.Current.HttpContext.Items[typeof(BatchedServerMessenger).Name]; - - list.Add(new Message - { - DispatchType = dispatchType, - IdArrayType = idArrayType, - Ids = ids, - JsonPayload = jsonPayload, - Refresher = refresher, - Servers = servers - }); - } - - private RefreshInstruction[] ConvertToInstruction(Message msg) - { - switch (msg.DispatchType) - { - case MessageType.RefreshAll: - return new[] - { - new RefreshInstruction - { - RefreshType = RefreshInstruction.RefreshMethodType.RefreshAll, - RefresherId = msg.Refresher.UniqueIdentifier - } - }; - case MessageType.RefreshById: - if (msg.IdArrayType == null) - { - throw new InvalidOperationException("Cannot refresh by id if the idArrayType is null"); - } - - if (msg.IdArrayType == typeof(int)) - { - var serializer = new JavaScriptSerializer(); - var jsonIds = serializer.Serialize(msg.Ids.Cast().ToArray()); - - return new[] - { - new RefreshInstruction - { - JsonIds = jsonIds, - RefreshType = RefreshInstruction.RefreshMethodType.RefreshByIds, - RefresherId = msg.Refresher.UniqueIdentifier - } - }; - } - - return msg.Ids.Select(x => new RefreshInstruction - { - GuidId = (Guid)x, - RefreshType = RefreshInstruction.RefreshMethodType.RefreshById, - RefresherId = msg.Refresher.UniqueIdentifier - }).ToArray(); - - case MessageType.RefreshByJson: - return new[] - { - new RefreshInstruction - { - RefreshType = RefreshInstruction.RefreshMethodType.RefreshByJson, - RefresherId = msg.Refresher.UniqueIdentifier, - JsonPayload = msg.JsonPayload - } - }; - case MessageType.RemoveById: - return msg.Ids.Select(x => new RefreshInstruction - { - IntId = (int)x, - RefreshType = RefreshInstruction.RefreshMethodType.RemoveById, - RefresherId = msg.Refresher.UniqueIdentifier - }).ToArray(); - case MessageType.RefreshByInstance: - case MessageType.RemoveByInstance: - default: - throw new ArgumentOutOfRangeException(); - } - } - - private void SendMessages(IEnumerable messages) - { - var batchedMsg = new List>(); - foreach (var msg in messages) - { - var instructions = ConvertToInstruction(msg); - batchedMsg.Add(new Tuple(msg, instructions)); - } - - var servers = batchedMsg.SelectMany(x => x.Item1.Servers).Distinct(); - - try - { - - //TODO: We should try to figure out the current server's address and if it matches any of the ones - // in the ServerAddress list, then just refresh directly on this server and exclude that server address - // from the list, this will save an internal request. - - using (var cacheRefresher = new ServerSyncWebServiceClient()) - { - var asyncResultsList = new List(); - - LogStartDispatch(); - - // Go through each configured node submitting a request asynchronously - //NOTE: 'asynchronously' in this case does not mean that it will continue while we give the page back to the user! - foreach (var server in servers) - { - //set the server address - cacheRefresher.Url = server.ServerAddress; - - var instructions = batchedMsg - .Where(x => x.Item1.Servers.Contains(server)) - .SelectMany(x => x.Item2) - //only execute distinct instructions - no sense in running the same one. - .Distinct() - .ToArray(); - - //Create a hash of the server name and the IIS app Id to send up so we don't double cache refresh the - // master server. - //Fixes: http://issues.umbraco.org/issue/U4-5491 - //NOTE: This will only work in full trust, in med trust, a double cache refresh is inevitable - var hashedAppId = string.Empty; - if (SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) - { - var hasher = new HashCodeCombiner(); - hasher.AddCaseInsensitiveString(NetworkHelper.MachineName); - hasher.AddCaseInsensitiveString(HttpRuntime.AppDomainAppId); - hashedAppId = hasher.GetCombinedHashCode(); - } - - asyncResultsList.Add( - cacheRefresher.BeginBulkRefresh( - instructions, - hashedAppId, - Login, Password, null, null)); - } - - var waitHandlesList = asyncResultsList.Select(x => x.AsyncWaitHandle).ToArray(); - - var errorCount = 0; - - //Wait for all requests to complete - WaitHandle.WaitAll(waitHandlesList.ToArray()); - - foreach (var t in asyncResultsList) - { - //var handleIndex = WaitHandle.WaitAny(waitHandlesList.ToArray(), TimeSpan.FromSeconds(15)); - - try - { - cacheRefresher.EndBulkRefresh(t); - } - catch (WebException ex) - { - LogDispatchNodeError(ex); - errorCount++; - } - catch (Exception ex) - { - LogDispatchNodeError(ex); - errorCount++; - } - } - LogDispatchBatchResult(errorCount); - } - } - catch (Exception ee) - { - LogDispatchBatchError(ee); - } - } - - private void LogDispatchBatchError(Exception ee) - { - LogHelper.Error("Error refreshing distributed list", ee); - } - - private void LogDispatchBatchResult(int errorCount) - { - LogHelper.Debug(string.Format("Distributed server push completed with {0} nodes reporting an error", errorCount == 0 ? "no" : errorCount.ToString(CultureInfo.InvariantCulture))); - } - - private void LogDispatchNodeError(Exception ex) - { - LogHelper.Error("Error refreshing a node in the distributed list", ex); - } - - private void LogDispatchNodeError(WebException ex) - { - string url = (ex.Response != null) ? ex.Response.ResponseUri.ToString() : "invalid url (responseUri null)"; - LogHelper.Error("Error refreshing a node in the distributed list, URI attempted: " + url, ex); - } - - private void LogStartDispatch() - { - LogHelper.Info("Submitting calls to distributed servers"); - } - } -} diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs new file mode 100644 index 0000000000..1789f92d9a --- /dev/null +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Sync; + +namespace Umbraco.Web +{ + internal class BatchedWebServiceServerMessenger : Core.Sync.BatchedWebServiceServerMessenger + { + internal BatchedWebServiceServerMessenger() + : base(GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + internal BatchedWebServiceServerMessenger(string login, string password) + : base(login, password, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls) + : base(login, password, useDistributedCalls, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + public BatchedWebServiceServerMessenger(Func> getLoginAndPassword) + : base(getLoginAndPassword, GetBatch) + { + UmbracoModule.EndRequest += UmbracoModule_EndRequest; + } + + private static ICollection GetBatch(bool ensure) + { + var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; + if (httpContext == null) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + + var key = typeof(BatchedWebServiceServerMessenger).Name; + + // no thread-safety here because it'll run in only 1 thread (request) at a time + var batch = (ICollection)httpContext.Items[key]; + if (batch == null && ensure) + httpContext.Items[key] = batch = new List(); + return batch; + } + + void UmbracoModule_EndRequest(object sender, EventArgs e) + { + FlushBatch(); + } + + protected override void ProcessBatch(RefreshInstructionEnvelope[] batch) + { + Message(batch); + } + } +} diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index bb0d4821b8..db67600757 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -1,38 +1,20 @@ using System; -using System.Collections.Generic; -using System.Globalization; using System.Linq; -using System.Net; -using System.Threading; -using System.Web.Services.Protocols; -using System.Xml; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.Sync; -using umbraco.BusinessLogic; using umbraco.interfaces; namespace Umbraco.Web.Cache { - //public class CacheUpdatedEventArgs : EventArgs - //{ - - //} - /// - /// DistributedCache is used to invalidate cache throughout the application which also takes in to account load balancing environments automatically + /// Represents the entry point into Umbraco's distributed cache infrastructure. /// /// - /// Distributing calls to all registered load balanced servers, ensuring that content are synced and cached on all servers. - /// Dispatcher is exendable, so 3rd party services can easily be integrated into the workflow, using the interfaces.ICacheRefresher interface. - /// - /// Dispatcher can refresh/remove content, templates and macros. + /// The distributed cache infrastructure ensures that distributed caches are + /// invalidated properly in load balancing environments. /// public sealed class DistributedCache { - #region Public constants/Ids public const string ApplicationTreeCacheRefresherId = "0AC6C028-9860-4EA4-958D-14D39F45886E"; @@ -56,21 +38,45 @@ namespace Umbraco.Web.Cache public const string DictionaryCacheRefresherId = "D1D7E227-F817-4816-BFE9-6C39B6152884"; public const string PublicAccessCacheRefresherId = "1DB08769-B104-4F8B-850E-169CAC1DF2EC"; + public static readonly Guid ApplicationTreeCacheRefresherGuid = new Guid(ApplicationTreeCacheRefresherId); + public static readonly Guid ApplicationCacheRefresherGuid = new Guid(ApplicationCacheRefresherId); + public static readonly Guid TemplateRefresherGuid = new Guid(TemplateRefresherId); + public static readonly Guid PageCacheRefresherGuid = new Guid(PageCacheRefresherId); + public static readonly Guid UnpublishedPageCacheRefresherGuid = new Guid(UnpublishedPageCacheRefresherId); + public static readonly Guid MemberCacheRefresherGuid = new Guid(MemberCacheRefresherId); + public static readonly Guid MemberGroupCacheRefresherGuid = new Guid(MemberGroupCacheRefresherId); + public static readonly Guid MediaCacheRefresherGuid = new Guid(MediaCacheRefresherId); + public static readonly Guid MacroCacheRefresherGuid = new Guid(MacroCacheRefresherId); + public static readonly Guid UserCacheRefresherGuid = new Guid(UserCacheRefresherId); + public static readonly Guid UserPermissionsCacheRefresherGuid = new Guid(UserPermissionsCacheRefresherId); + public static readonly Guid UserTypeCacheRefresherGuid = new Guid(UserTypeCacheRefresherId); + public static readonly Guid ContentTypeCacheRefresherGuid = new Guid(ContentTypeCacheRefresherId); + public static readonly Guid LanguageCacheRefresherGuid = new Guid(LanguageCacheRefresherId); + public static readonly Guid DomainCacheRefresherGuid = new Guid(DomainCacheRefresherId); + public static readonly Guid StylesheetCacheRefresherGuid = new Guid(StylesheetCacheRefresherId); + public static readonly Guid StylesheetPropertyCacheRefresherGuid = new Guid(StylesheetPropertyCacheRefresherId); + public static readonly Guid DataTypeCacheRefresherGuid = new Guid(DataTypeCacheRefresherId); + public static readonly Guid DictionaryCacheRefresherGuid = new Guid(DictionaryCacheRefresherId); + public static readonly Guid PublicAccessCacheRefresherGuid = new Guid(PublicAccessCacheRefresherId); + #endregion + #region Constructor & Singleton + + // note - should inject into the application instead of using a singleton private static readonly DistributedCache InstanceObject = new DistributedCache(); /// - /// Constructor + /// Initializes a new instance of the class. /// private DistributedCache() - { - } + { } /// - /// Singleton + /// Gets the static unique instance of the class. /// - /// + /// The static unique instance of the class. + /// Exists so that extension methods can be added to the distributed cache. public static DistributedCache Instance { get @@ -79,22 +85,25 @@ namespace Umbraco.Web.Cache } } + #endregion + + #region Core notification methods + /// - /// Sends a request to all registered load-balanced servers to refresh node with the specified Id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of specifieds item invalidation, for a specified . /// - /// - /// - /// The callback method to retrieve the ID from an instance - /// The instances containing Ids + /// The type of the invalidated items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The invalidated items. /// - /// This method is much better for performance because it does not need to re-lookup an object instance + /// This method is much better for performance because it does not need to re-lookup object instances. /// public void Refresh(Guid factoryGuid, Func getNumericId, params T[] instances) { if (factoryGuid == Guid.Empty || instances.Length == 0 || getNumericId == null) return; - ServerMessengerResolver.Current.Messenger.PerformRefresh( + ServerMessengerResolver.Current.Messenger.PerformRefresh( ServerRegistrarResolver.Current.Registrar.Registrations, GetRefresherById(factoryGuid), getNumericId, @@ -102,11 +111,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh node with the specified Id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher used to refresh the node. - /// The id of the node. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. public void Refresh(Guid factoryGuid, int id) { if (factoryGuid == Guid.Empty || id == default(int)) return; @@ -118,11 +126,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh the node with the specified guid - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item invalidation, for a specified . /// - /// The unique identifier of the ICacheRefresher used to refresh the node. - /// The guid of the node. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the invalidated item. public void Refresh(Guid factoryGuid, Guid id) { if (factoryGuid == Guid.Empty || id == Guid.Empty) return; @@ -134,11 +141,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to refresh data based on the custom json payload - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache, for a specified . /// - /// - /// + /// The unique identifier of the ICacheRefresher. + /// The notification content. public void RefreshByJson(Guid factoryGuid, string jsonPayload) { if (factoryGuid == Guid.Empty || jsonPayload.IsNullOrWhiteSpace()) return; @@ -149,26 +155,37 @@ namespace Umbraco.Web.Cache jsonPayload); } + ///// + ///// Notifies the distributed cache, for a specified . + ///// + ///// The unique identifier of the ICacheRefresher. + ///// The notification content. + //internal void Notify(Guid refresherId, object payload) + //{ + // if (refresherId == Guid.Empty || payload == null) return; + + // ServerMessengerResolver.Current.Messenger.Notify( + // ServerRegistrarResolver.Current.Registrar.Registrations, + // GetRefresherById(refresherId), + // json); + //} + /// - /// Sends a request to all registered load-balanced servers to refresh all nodes - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a global invalidation for a specified . /// - /// The unique identifier. + /// The unique identifier of the ICacheRefresher. public void RefreshAll(Guid factoryGuid) { if (factoryGuid == Guid.Empty) return; - RefreshAll(factoryGuid, true); } /// - /// Sends a request to all registered load-balanced servers to refresh all nodes - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a global invalidation for a specified . /// - /// The unique identifier. - /// - /// If true will send the request out to all registered LB servers, if false will only execute the current server - /// + /// The unique identifier of the ICacheRefresher. + /// If true, all servers in the load balancing environment are notified; otherwise, + /// only the local server is notified. public void RefreshAll(Guid factoryGuid, bool allServers) { if (factoryGuid == Guid.Empty) return; @@ -181,11 +198,10 @@ namespace Umbraco.Web.Cache } /// - /// Sends a request to all registered load-balanced servers to remove the node with the specified id - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of a specified item removal, for a specified . /// - /// The unique identifier. - /// The id. + /// The unique identifier of the ICacheRefresher. + /// The unique identifier of the removed item. public void Remove(Guid factoryGuid, int id) { if (factoryGuid == Guid.Empty || id == default(int)) return; @@ -195,28 +211,32 @@ namespace Umbraco.Web.Cache GetRefresherById(factoryGuid), id); } - + /// - /// Sends a request to all registered load-balanced servers to remove the node specified - /// using the specified ICacheRefresher with the guid factoryGuid. + /// Notifies the distributed cache of specifieds item removal, for a specified . /// - /// - /// - /// - /// + /// The type of the removed items. + /// The unique identifier of the ICacheRefresher. + /// A function returning the unique identifier of items. + /// The removed items. + /// + /// This method is much better for performance because it does not need to re-lookup object instances. + /// public void Remove(Guid factoryGuid, Func getNumericId, params T[] instances) { - ServerMessengerResolver.Current.Messenger.PerformRemove( + ServerMessengerResolver.Current.Messenger.PerformRemove( ServerRegistrarResolver.Current.Registrar.Registrations, GetRefresherById(factoryGuid), getNumericId, instances); - } + } + #endregion + + // helper method to get an ICacheRefresher by its unique identifier private static ICacheRefresher GetRefresherById(Guid uniqueIdentifier) { return CacheRefreshersResolver.Current.GetById(uniqueIdentifier); } - } } diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 3e053fcd18..c3c1d64d89 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Events; @@ -11,7 +9,7 @@ using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Cache { /// - /// Extension methods for DistrubutedCache + /// Extension methods for /// internal static class DistributedCacheExtensions { @@ -19,628 +17,383 @@ namespace Umbraco.Web.Cache public static void RefreshPublicAccess(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.PublicAccessCacheRefresherId)); + dc.RefreshAll(DistributedCache.PublicAccessCacheRefresherGuid); } - #endregion #region Application tree cache + public static void RefreshAllApplicationTreeCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.ApplicationTreeCacheRefresherId)); + dc.RefreshAll(DistributedCache.ApplicationTreeCacheRefresherGuid); } + #endregion #region Application cache + public static void RefreshAllApplicationCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.ApplicationCacheRefresherId)); + dc.RefreshAll(DistributedCache.ApplicationCacheRefresherGuid); } + #endregion #region User type cache + public static void RemoveUserTypeCache(this DistributedCache dc, int userTypeId) { - dc.Remove(new Guid(DistributedCache.UserTypeCacheRefresherId), userTypeId); + dc.Remove(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); } public static void RefreshUserTypeCache(this DistributedCache dc, int userTypeId) { - dc.Refresh(new Guid(DistributedCache.UserTypeCacheRefresherId), userTypeId); + dc.Refresh(DistributedCache.UserTypeCacheRefresherGuid, userTypeId); } public static void RefreshAllUserTypeCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserTypeCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserTypeCacheRefresherGuid); } + #endregion #region User cache + public static void RemoveUserCache(this DistributedCache dc, int userId) { - dc.Remove(new Guid(DistributedCache.UserCacheRefresherId), userId); + dc.Remove(DistributedCache.UserCacheRefresherGuid, userId); } public static void RefreshUserCache(this DistributedCache dc, int userId) { - dc.Refresh(new Guid(DistributedCache.UserCacheRefresherId), userId); + dc.Refresh(DistributedCache.UserCacheRefresherGuid, userId); } public static void RefreshAllUserCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserCacheRefresherGuid); } + #endregion #region User permissions cache + public static void RemoveUserPermissionsCache(this DistributedCache dc, int userId) { - dc.Remove(new Guid(DistributedCache.UserPermissionsCacheRefresherId), userId); + dc.Remove(DistributedCache.UserPermissionsCacheRefresherGuid, userId); } public static void RefreshUserPermissionsCache(this DistributedCache dc, int userId) { - dc.Refresh(new Guid(DistributedCache.UserPermissionsCacheRefresherId), userId); + dc.Refresh(DistributedCache.UserPermissionsCacheRefresherGuid, userId); } public static void RefreshAllUserPermissionsCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.UserPermissionsCacheRefresherId)); + dc.RefreshAll(DistributedCache.UserPermissionsCacheRefresherGuid); } + #endregion #region Template cache - /// - /// Refreshes the cache amongst servers for a template - /// - /// - /// + public static void RefreshTemplateCache(this DistributedCache dc, int templateId) { - dc.Refresh(new Guid(DistributedCache.TemplateRefresherId), templateId); + dc.Refresh(DistributedCache.TemplateRefresherGuid, templateId); } - /// - /// Removes the cache amongst servers for a template - /// - /// - /// public static void RemoveTemplateCache(this DistributedCache dc, int templateId) { - dc.Remove(new Guid(DistributedCache.TemplateRefresherId), templateId); + dc.Remove(DistributedCache.TemplateRefresherGuid, templateId); } #endregion #region Dictionary cache - /// - /// Refreshes the cache amongst servers for a dictionary item - /// - /// - /// + public static void RefreshDictionaryCache(this DistributedCache dc, int dictionaryItemId) { - dc.Refresh(new Guid(DistributedCache.DictionaryCacheRefresherId), dictionaryItemId); + dc.Refresh(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); } - /// - /// Refreshes the cache amongst servers for a dictionary item - /// - /// - /// public static void RemoveDictionaryCache(this DistributedCache dc, int dictionaryItemId) { - dc.Remove(new Guid(DistributedCache.DictionaryCacheRefresherId), dictionaryItemId); + dc.Remove(DistributedCache.DictionaryCacheRefresherGuid, dictionaryItemId); } #endregion #region Data type cache - /// - /// Refreshes the cache amongst servers for a data type - /// - /// - /// + public static void RefreshDataTypeCache(this DistributedCache dc, global::umbraco.cms.businesslogic.datatype.DataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Removes the cache amongst servers for a data type - /// - /// - /// public static void RemoveDataTypeCache(this DistributedCache dc, global::umbraco.cms.businesslogic.datatype.DataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Refreshes the cache amongst servers for a data type - /// - /// - /// public static void RefreshDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } - /// - /// Removes the cache amongst servers for a data type - /// - /// - /// public static void RemoveDataTypeCache(this DistributedCache dc, IDataTypeDefinition dataType) { - if (dataType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.DataTypeCacheRefresherId), - DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); - } + if (dataType == null) return; + dc.RefreshByJson(DistributedCache.DataTypeCacheRefresherGuid, DataTypeCacheRefresher.SerializeToJsonPayload(dataType)); } #endregion #region Page cache - /// - /// Refreshes the cache amongst servers for all pages - /// - /// + public static void RefreshAllPageCache(this DistributedCache dc) { - dc.RefreshAll(new Guid(DistributedCache.PageCacheRefresherId)); + dc.RefreshAll(DistributedCache.PageCacheRefresherGuid); } - /// - /// Refreshes the cache amongst servers for a page - /// - /// - /// public static void RefreshPageCache(this DistributedCache dc, int documentId) { - dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), documentId); + dc.Refresh(DistributedCache.PageCacheRefresherGuid, documentId); } - /// - /// Refreshes page cache for all instances passed in - /// - /// - /// public static void RefreshPageCache(this DistributedCache dc, params IContent[] content) { - dc.Refresh(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); + dc.Refresh(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); } - /// - /// Removes the cache amongst servers for a page - /// - /// - /// public static void RemovePageCache(this DistributedCache dc, params IContent[] content) { - dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), x => x.Id, content); + dc.Remove(DistributedCache.PageCacheRefresherGuid, x => x.Id, content); } - /// - /// Removes the cache amongst servers for a page - /// - /// - /// public static void RemovePageCache(this DistributedCache dc, int documentId) { - dc.Remove(new Guid(DistributedCache.PageCacheRefresherId), documentId); + dc.Remove(DistributedCache.PageCacheRefresherGuid, documentId); } - /// - /// invokes the unpublished page cache refresher - /// - /// - /// public static void RefreshUnpublishedPageCache(this DistributedCache dc, params IContent[] content) { - dc.Refresh(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + dc.Refresh(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); } - /// - /// invokes the unpublished page cache refresher - /// - /// - /// public static void RemoveUnpublishedPageCache(this DistributedCache dc, params IContent[] content) { - dc.Remove(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), x => x.Id, content); + dc.Remove(DistributedCache.UnpublishedPageCacheRefresherGuid, x => x.Id, content); } - /// - /// invokes the unpublished page cache refresher to mark all ids for permanent removal - /// - /// - /// public static void RemoveUnpublishedCachePermanently(this DistributedCache dc, params int[] contentIds) { - dc.RefreshByJson(new Guid(DistributedCache.UnpublishedPageCacheRefresherId), - UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); + dc.RefreshByJson(DistributedCache.UnpublishedPageCacheRefresherGuid, UnpublishedPageCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(contentIds)); } #endregion #region Member cache - /// - /// Refreshes the cache among servers for a member - /// - /// - /// public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); } - /// - /// Removes the cache among servers for a member - /// - /// - /// public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members) { - dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), x => x.Id, members); + dc.Remove(DistributedCache.MemberCacheRefresherGuid, x => x.Id, members); } - /// - /// Refreshes the cache among servers for a member - /// - /// - /// [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] public static void RefreshMemberCache(this DistributedCache dc, int memberId) { - dc.Refresh(new Guid(DistributedCache.MemberCacheRefresherId), memberId); + dc.Refresh(DistributedCache.MemberCacheRefresherGuid, memberId); } - /// - /// Removes the cache among servers for a member - /// - /// - /// [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] public static void RemoveMemberCache(this DistributedCache dc, int memberId) { - dc.Remove(new Guid(DistributedCache.MemberCacheRefresherId), memberId); + dc.Remove(DistributedCache.MemberCacheRefresherGuid, memberId); } #endregion #region Member group cache - /// - /// Refreshes the cache among servers for a member group - /// - /// - /// + public static void RefreshMemberGroupCache(this DistributedCache dc, int memberGroupId) { - dc.Refresh(new Guid(DistributedCache.MemberGroupCacheRefresherId), memberGroupId); + dc.Refresh(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); } - /// - /// Removes the cache among servers for a member group - /// - /// - /// public static void RemoveMemberGroupCache(this DistributedCache dc, int memberGroupId) { - dc.Remove(new Guid(DistributedCache.MemberGroupCacheRefresherId), memberGroupId); + dc.Remove(DistributedCache.MemberGroupCacheRefresherGuid, memberGroupId); } #endregion #region Media Cache - /// - /// Refreshes the cache amongst servers for media items - /// - /// - /// public static void RefreshMediaCache(this DistributedCache dc, params IMedia[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayload(MediaCacheRefresher.OperationType.Saved, media)); } - /// - /// Refreshes the cache amongst servers for a media item after it's been moved - /// - /// - /// public static void RefreshMediaCacheAfterMoving(this DistributedCache dc, params MoveEventInfo[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForMoving( - MediaCacheRefresher.OperationType.Saved, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Saved, media)); } - /// - /// Removes the cache amongst servers for a media item - /// - /// - /// - /// - /// 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. - /// + // 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); } - /// - /// Removes the cache among servers for media items when they are recycled - /// - /// - /// public static void RemoveMediaCacheAfterRecycling(this DistributedCache dc, params MoveEventInfo[] media) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForMoving( - MediaCacheRefresher.OperationType.Trashed, media)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForMoving(MediaCacheRefresher.OperationType.Trashed, media)); } - /// - /// Removes the cache among servers for media items when they are permanently deleted - /// - /// - /// public static void RemoveMediaCachePermanently(this DistributedCache dc, params int[] mediaIds) { - dc.RefreshByJson(new Guid(DistributedCache.MediaCacheRefresherId), - MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); + dc.RefreshByJson(DistributedCache.MediaCacheRefresherGuid, MediaCacheRefresher.SerializeToJsonPayloadForPermanentDeletion(mediaIds)); } #endregion #region Macro Cache - /// - /// Clears the cache for all macros on the current server - /// - /// public static void ClearAllMacroCacheOnCurrentServer(this DistributedCache dc) { - //NOTE: The 'false' ensure that it will only refresh on the current server, not post to all servers - dc.RefreshAll(new Guid(DistributedCache.MacroCacheRefresherId), false); + // NOTE: The 'false' ensure that it will only refresh on the current server, not post to all servers + dc.RefreshAll(DistributedCache.MacroCacheRefresherGuid, false); } - /// - /// Refreshes the cache amongst servers for a macro item - /// - /// - /// public static void RefreshMacroCache(this DistributedCache dc, IMacro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, IMacro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Refreshes the cache amongst servers for a macro item - /// - /// - /// public static void RefreshMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, global::umbraco.cms.businesslogic.macro.Macro macro) { - if (macro != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } - /// - /// Removes the cache amongst servers for a macro item - /// - /// - /// public static void RemoveMacroCache(this DistributedCache dc, macro macro) { - if (macro != null && macro.Model != null) - { - dc.RefreshByJson(new Guid(DistributedCache.MacroCacheRefresherId), - MacroCacheRefresher.SerializeToJsonPayload(macro)); - } + if (macro == null || macro.Model == null) return; + dc.RefreshByJson(DistributedCache.MacroCacheRefresherGuid, MacroCacheRefresher.SerializeToJsonPayload(macro)); } + #endregion #region Document type cache - /// - /// Remove all cache for a given content type - /// - /// - /// public static void RefreshContentTypeCache(this DistributedCache dc, IContentType contentType) { - if (contentType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); - } + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, contentType)); } - /// - /// Remove all cache for a given content type - /// - /// - /// public static void RemoveContentTypeCache(this DistributedCache dc, IContentType contentType) { - if (contentType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); - } + if (contentType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, contentType)); } #endregion #region Media type cache - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RefreshMediaTypeCache(this DistributedCache dc, IMediaType mediaType) { - if (mediaType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); - } + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, mediaType)); } - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RemoveMediaTypeCache(this DistributedCache dc, IMediaType mediaType) { - if (mediaType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); - } + if (mediaType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(true, mediaType)); } #endregion #region Media type cache - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RefreshMemberTypeCache(this DistributedCache dc, IMemberType memberType) { - if (memberType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); - } + if (memberType == null) return; + dc.RefreshByJson(DistributedCache.ContentTypeCacheRefresherGuid, ContentTypeCacheRefresher.SerializeToJsonPayload(false, memberType)); } - /// - /// Remove all cache for a given media type - /// - /// - /// public static void RemoveMemberTypeCache(this DistributedCache dc, IMemberType memberType) { - if (memberType != null) - { - dc.RefreshByJson(new Guid(DistributedCache.ContentTypeCacheRefresherId), - ContentTypeCacheRefresher.SerializeToJsonPayload(true, 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) - { - dc.Refresh(new Guid(DistributedCache.StylesheetPropertyCacheRefresherId), styleSheetProperty.Id); - } + 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) - { - dc.Remove(new Guid(DistributedCache.StylesheetPropertyCacheRefresherId), styleSheetProperty.Id); - } + if (styleSheetProperty == null) return; + dc.Remove(DistributedCache.StylesheetPropertyCacheRefresherGuid, styleSheetProperty.Id); } public static void RefreshStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) { - if (styleSheet != null) - { - dc.Refresh(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Refresh(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } public static void RemoveStylesheetCache(this DistributedCache dc, StyleSheet styleSheet) { - if (styleSheet != null) - { - dc.Remove(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + 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) - { - dc.Refresh(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + 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) - { - dc.Remove(new Guid(DistributedCache.StylesheetCacheRefresherId), styleSheet.Id); - } + if (styleSheet == null) return; + dc.Remove(DistributedCache.StylesheetCacheRefresherGuid, styleSheet.Id); } #endregion @@ -649,18 +402,14 @@ namespace Umbraco.Web.Cache public static void RefreshDomainCache(this DistributedCache dc, IDomain domain) { - if (domain != null) - { - dc.Refresh(new Guid(DistributedCache.DomainCacheRefresherId), domain.Id); - } + if (domain == null) return; + dc.Refresh(DistributedCache.DomainCacheRefresherGuid, domain.Id); } public static void RemoveDomainCache(this DistributedCache dc, IDomain domain) { - if (domain != null) - { - dc.Remove(new Guid(DistributedCache.DomainCacheRefresherId), domain.Id); - } + if (domain == null) return; + dc.Remove(DistributedCache.DomainCacheRefresherGuid, domain.Id); } #endregion @@ -669,44 +418,38 @@ namespace Umbraco.Web.Cache public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) { - if (language != null) - { - dc.Refresh(new Guid(DistributedCache.LanguageCacheRefresherId), language.Id); - } + if (language == null) return; + dc.Refresh(DistributedCache.LanguageCacheRefresherGuid, language.Id); } public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) { - if (language != null) - { - dc.Remove(new Guid(DistributedCache.LanguageCacheRefresherId), language.Id); - } + 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) - { - dc.Refresh(new Guid(DistributedCache.LanguageCacheRefresherId), language.id); - } + 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) - { - dc.Remove(new Guid(DistributedCache.LanguageCacheRefresherId), language.id); - } + 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) - { - ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); - } + if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration <= 0) return; + ApplicationContext.Current.ApplicationCache.ClearCacheObjectTypes("MS.Internal.Xml.XPath.XPathSelectionIterator"); } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs index 9060b9f773..fd09f5ff8c 100644 --- a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs +++ b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs @@ -1,14 +1,44 @@ namespace Umbraco.Web.Routing { /// - /// Reasons a request was not routable on the front-end + /// Represents the outcome of trying to route an incoming request. /// internal enum EnsureRoutableOutcome { + /// + /// Request routes to a document. + /// + /// + /// Umbraco was ready and configured, and has content. + /// The request looks like it can be a route to a document. This does not + /// mean that there *is* a matching document, ie the request might end up returning + /// 404. + /// IsRoutable = 0, + + /// + /// Request does not route to a document. + /// + /// + /// Umbraco was ready and configured, and has content. + /// The request does not look like it can be a route to a document. Can be + /// anything else eg back-office, surface controller... + /// NotDocumentRequest = 10, + + /// + /// Umbraco was not ready. + /// NotReady = 11, + + /// + /// Umbraco was not configured. + /// NotConfigured = 12, + + /// + /// There was no content at all. + /// NoContent = 13 } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs index ab478c6c2b..6d8faa782f 100644 --- a/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs +++ b/src/Umbraco.Web/Strategies/ServerRegistrationEventHandler.cs @@ -1,149 +1,95 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; using System.Web; +using Newtonsoft.Json; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Web.Routing; namespace Umbraco.Web.Strategies { /// - /// This will ensure that the server is automatically registered in the database as an active node - /// on application startup and whenever a back office request occurs. + /// Ensures that servers are automatically registered in the database, when using the database server registrar. /// /// - /// We do this on app startup to ensure that the server is in the database but we also do it for the first 'x' times - /// a back office request is made so that we can tell if they are using https protocol which would update to that address - /// in the database. The first front-end request probably wouldn't be an https request. - /// - /// For back office requests (so that we don't constantly make db calls), we'll only update the database when we detect at least - /// a timespan of 1 minute between requests. + /// At the moment servers are automatically registered upon first request and then on every + /// request but not more than once per (configurable) period. This really is "for information & debug" purposes so + /// we can look at the table and see what servers are registered - but the info is not used anywhere. + /// Should we actually want to use this, we would need a better and more deterministic way of figuring + /// out the "server address" ie the address to which server-to-server requests should be sent - because it + /// probably is not the "current request address" - especially in multi-domains configurations. /// public sealed class ServerRegistrationEventHandler : ApplicationEventHandler { - private static bool _initUpdated = false; private static DateTime _lastUpdated = DateTime.MinValue; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - - //protected override void ApplicationStarting(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - //{ - // ServerRegistrarResolver.Current.SetServerRegistrar( - // new DatabaseServerRegistrar( - // new Lazy(() => applicationContext.Services.ServerRegistrationService))); - //} - - /// - /// Update the database with this entry and bind to request events - /// - /// - /// + // bind to events protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { - //no need to bind to the event if we are not actually using the database server registrar + // only for the DatabaseServerRegistrar if (ServerRegistrarResolver.Current.Registrar is DatabaseServerRegistrar) - { - //bind to event - UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; - } + UmbracoModule.RouteAttempt += UmbracoModuleRouteAttempt; } - - static void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) + // handles route attempts. + private static void UmbracoModuleRouteAttempt(object sender, RoutableAttemptEventArgs e) { if (e.HttpContext.Request == null || e.HttpContext.Request.Url == null) return; - if (e.Outcome == EnsureRoutableOutcome.IsRoutable) + switch (e.Outcome) { - using (var lck = new UpgradeableReadLock(Locker)) - { - //we only want to do the initial update once - if (!_initUpdated) - { - lck.UpgradeToWriteLock(); - _initUpdated = true; - UpdateServerEntry(e.HttpContext, e.UmbracoContext.Application); - return; - } - } - } - - //if it is not a document request, we'll check if it is a back end request - if (e.Outcome == EnsureRoutableOutcome.NotDocumentRequest) - { - //check if this is in the umbraco back office - if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) - { - //yup it's a back office request! - using (var lck = new UpgradeableReadLock(Locker)) - { - //we don't want to update if it's not been at least a minute since last time - var isItAMinute = DateTime.Now.Subtract(_lastUpdated).TotalSeconds >= 60; - if (isItAMinute) - { - lck.UpgradeToWriteLock(); - _initUpdated = true; - _lastUpdated = DateTime.Now; - UpdateServerEntry(e.HttpContext, e.UmbracoContext.Application); - } - } - } + case EnsureRoutableOutcome.IsRoutable: + // front-end request + RegisterServer(e); + break; + case EnsureRoutableOutcome.NotDocumentRequest: + // anything else (back-end request, service...) + //so it's not a document request, we'll check if it's a back office request + if (e.HttpContext.Request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) + RegisterServer(e); + break; + /* + case EnsureRoutableOutcome.NotReady: + case EnsureRoutableOutcome.NotConfigured: + case EnsureRoutableOutcome.NoContent: + default: + // otherwise, do nothing + break; + */ } } - - private static void UpdateServerEntry(HttpContextBase httpContext, ApplicationContext applicationContext) + // register current server (throttled). + private static void RegisterServer(UmbracoRequestEventArgs e) { + var reg = (DatabaseServerRegistrar) ServerRegistrarResolver.Current.Registrar; + var options = reg.Options; + var secondsSinceLastUpdate = DateTime.Now.Subtract(_lastUpdated).TotalSeconds; + if (secondsSinceLastUpdate < options.ThrottleSeconds) return; + + _lastUpdated = DateTime.Now; + + var url = e.HttpContext.Request.Url; + var svc = e.UmbracoContext.Application.Services.ServerRegistrationService; + try { - var address = httpContext.Request.Url.GetLeftPart(UriPartial.Authority); - applicationContext.Services.ServerRegistrationService.EnsureActive(address); + if (url == null) + throw new Exception("Request.Url is null."); + + var serverAddress = url.GetLeftPart(UriPartial.Authority); + var serverIdentity = JsonConvert.SerializeObject(new + { + machineName = NetworkHelper.MachineName, + appDomainAppId = HttpRuntime.AppDomainAppId + }); + + svc.TouchServer(serverAddress, serverIdentity, options.StaleServerTimeout); } - catch (Exception e) + catch (Exception ex) { - LogHelper.Error("Failed to update server record in database.", e); + LogHelper.Error("Failed to update server record in database.", ex); } } - - //private static IEnumerable> GetBindings(HttpContextBase context) - //{ - // // Get the Site name - // string siteName = System.Web.Hosting.HostingEnvironment.SiteName; - - // // Get the sites section from the AppPool.config - // Microsoft.Web.Administration.ConfigurationSection sitesSection = - // Microsoft.Web.Administration.WebConfigurationManager.GetSection(null, null, "system.applicationHost/sites"); - - // foreach (Microsoft.Web.Administration.ConfigurationElement site in sitesSection.GetCollection()) - // { - // // Find the right Site - // if (String.Equals((string)site["name"], siteName, StringComparison.OrdinalIgnoreCase)) - // { - - // // For each binding see if they are http based and return the port and protocol - // foreach (Microsoft.Web.Administration.ConfigurationElement binding in site.GetCollection("bindings")) - // { - // string protocol = (string)binding["protocol"]; - // string bindingInfo = (string)binding["bindingInformation"]; - - // if (protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - // { - // string[] parts = bindingInfo.Split(':'); - // if (parts.Length == 3) - // { - // string port = parts[1]; - // yield return new KeyValuePair(protocol, port); - // } - // } - // } - // } - // } - //} } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1ce693b27a..4bb3b41651 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -132,6 +132,10 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.Web.Administration.7.0.0.0\lib\net20\Microsoft.Web.Administration.dll + True + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -277,6 +281,7 @@ + @@ -287,7 +292,8 @@ - + + @@ -1884,6 +1890,7 @@ True Reference.map + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 4a7b78c683..787db1f12c 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -11,6 +11,7 @@ using System.Web.Mvc; using System.Web.Routing; using ClientDependency.Core.Config; using Examine; +using umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; @@ -37,6 +38,7 @@ using Umbraco.Web.Scheduling; using Umbraco.Web.UI.JavaScript; using Umbraco.Web.WebApi; using umbraco.BusinessLogic; +using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine; @@ -49,7 +51,7 @@ namespace Umbraco.Web { private readonly bool _isForTesting; //NOTE: see the Initialize method for what this is used for - private List _indexesToRebuild = new List(); + private readonly List _indexesToRebuild = new List(); public WebBootManager(UmbracoApplicationBase umbracoApplication) : this(umbracoApplication, false) @@ -320,13 +322,18 @@ namespace Umbraco.Web //set the default RenderMvcController DefaultRenderMvcControllerResolver.Current = new DefaultRenderMvcControllerResolver(typeof(RenderMvcController)); - //Override the ServerMessengerResolver to set a username/password for the distributed calls - ServerMessengerResolver.Current.SetServerMessenger(new BatchedServerMessenger(() => + ServerMessengerResolver.Current.SetServerMessenger(new BatchedWebServiceServerMessenger(() => { //we should not proceed to change this if the app/database is not configured since there will // be no user, plus we don't need to have server messages sent if this is the case. if (ApplicationContext.IsConfigured && ApplicationContext.DatabaseContext.IsDatabaseConfigured) { + //disable if they are not enabled + if (UmbracoConfig.For.UmbracoSettings().DistributedCall.Enabled == false) + { + return null; + } + try { var user = User.GetUser(UmbracoConfig.For.UmbracoSettings().DistributedCall.UserId); diff --git a/src/Umbraco.Web/WebServerUtility.cs b/src/Umbraco.Web/WebServerUtility.cs new file mode 100644 index 0000000000..7ea9e8ea25 --- /dev/null +++ b/src/Umbraco.Web/WebServerUtility.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Hosting; +using Umbraco.Core; +using Microsoft.Web.Administration; + +namespace Umbraco.Web +{ + internal class WebServerUtility + { + // NOTE + // + // there's some confusion with Microsoft.Web.Administration versions + // 7.0.0.0 is installed by NuGet and will read IIS settings + // 7.9.0.0 comes with IIS Express and will read IIS Express + // we want to use 7.0.0.0 when building + // and then... there are further versions that are N/A on NuGet + // + // Umbraco uses 7.0.0.0 from NuGet + // IMPORTANT: and then, the reference's SpecificVersion property MUST be set to true + // otherwise we might build with 7.9.0.0 and end up in troubles (reading IIS Express + // instead of IIS even when running IIS) - IIS Express has a binding redirect from + // 7.0.0.0 to 7.9.0.0 so it's fine. + // + // read: + // http://stackoverflow.com/questions/11208270/microsoft-web-administration-servermanager-looking-in-wrong-directory-for-iisexp + // http://stackoverflow.com/questions/8467908/how-to-use-servermanager-to-read-iis-sites-not-iis-express-from-class-library + // http://stackoverflow.com/questions/25812169/microsoft-web-administration-servermanager-is-connecting-to-the-iis-express-inst + + public static IEnumerable GetBindings() + { + // FIXME + // which of these methods shall we use? + // what about permissions, trust, etc? + + //return GetBindings2(); + throw new NotImplementedException(); + } + + private static IEnumerable GetBindings1() + { + // get the site name + var siteName = HostingEnvironment.SiteName; + + // get the site from the sites section from the AppPool.config + var sitesSection = WebConfigurationManager.GetSection(null, null, "system.applicationHost/sites"); + var site = sitesSection.GetCollection().FirstOrDefault(x => ((string) x["name"]).InvariantEquals(siteName)); + if (site == null) + return Enumerable.Empty(); + + return site.GetCollection("bindings") + .Where(x => ((string) x["protocol"]).StartsWith("http", StringComparison.OrdinalIgnoreCase)) + .Select(x => + { + var bindingInfo = (string) x["bindingInformation"]; + var parts = bindingInfo.Split(':'); // can count be != 3 ?? + return new Uri(x["protocol"] + "://" + parts[2] + ":" + parts[1] + "/"); + }); + } + + private static IEnumerable GetBindings2() + { + // get the site name + var siteName = HostingEnvironment.SiteName; + + // get the site from the server manager + var mgr = new ServerManager(); + var site = mgr.Sites.FirstOrDefault(x => x.Name.InvariantEquals(siteName)); + if (site == null) + return Enumerable.Empty(); + + // get the bindings + return site.Bindings + .Where(x => x.Protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + .Select(x => new Uri(x.Protocol + "://" + x.Host + ":" + x.EndPoint.Port + "/")); + } + } +} diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index b08c9193ee..f624d5b6d5 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs index 5bcd3cab79..4186b2446a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -1,99 +1,104 @@ using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Diagnostics; using System.Linq; using System.Web; -using System.Web.Script.Serialization; using System.Web.Services; using System.Xml; +using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Sync; +using umbraco.interfaces; namespace umbraco.presentation.webservices { - /// - /// Summary description for CacheRefresher. + /// CacheRefresher web service. /// [WebService(Namespace="http://umbraco.org/webservices/")] public class CacheRefresher : WebService - { + { + #region Helpers - /// - /// This checks the passed in hash and verifies if it does not match the hash of the combination of appDomainAppId and machineName - /// passed in. If the hashes don't match, then cache refreshing continues. - /// - /// - /// - /// - /// - internal bool ContinueRefreshingForRequest(string hash, string appDomainAppId, string machineName) - { - //check if this is the same app id as the one passed in, if it is, then we will ignore - // the request - we will have to assume that the cache refreshing has already been applied to the server - // that executed the request. - if (hash.IsNullOrWhiteSpace() == false && SystemUtilities.GetCurrentTrustLevel() == AspNetHostingPermissionLevel.Unrestricted) - { - var hasher = new HashCodeCombiner(); - hasher.AddCaseInsensitiveString(machineName); - hasher.AddCaseInsensitiveString(appDomainAppId); - var hashedAppId = hasher.GetCombinedHashCode(); + // is the server originating from this server - ie are we self-messaging? + // in which case we should ignore the message because it's been processed locally already + internal static bool SelfMessage(string hash) + { + if (hash != WebServiceServerMessenger.GetCurrentServerHash()) return false; - //we can only check this in full trust. if it's in medium trust we'll just end up with - // the server refreshing it's cache twice. - if (hashedAppId == hash) - { - LogHelper.Debug( - "The passed in hashed appId equals the current server's hashed appId, cache refreshing will be ignored for this request as it will have already executed for this server (server: {0} , appId: {1} , hash: {2})", - () => machineName, - () => appDomainAppId, - () => hashedAppId); + LogHelper.Debug( + "Ignoring self-message. (server: {0}, appId: {1}, hash: {2})", + () => NetworkHelper.MachineName, + () => HttpRuntime.AppDomainAppId, + () => hash); - return false; - } - } + return true; + } - return true; - } + private static ICacheRefresher GetRefresher(Guid id) + { + var refresher = CacheRefreshersResolver.Current.GetById(id); + if (refresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + id + "\" does not exist."); + return refresher; + } + + private static IJsonCacheRefresher GetJsonRefresher(Guid id) + { + return GetJsonRefresher(GetRefresher(id)); + } + + private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) + { + var jsonRefresher = refresher as IJsonCacheRefresher; + if (jsonRefresher == null) + throw new InvalidOperationException("Cache refresher with ID \"" + refresher.UniqueIdentifier + "\" does not implement " + typeof(IJsonCacheRefresher) + "."); + return jsonRefresher; + } + + private static bool NotAutorized(string login, string rawPassword) + { + var user = ApplicationContext.Current.Services.UserService.GetByUsername(login); + return user == null || user.RawPasswordValue != rawPassword; + } + + #endregion [WebMethod] public void BulkRefresh(RefreshInstruction[] instructions, string appId, string login, string password) { - if (BusinessLogic.User.validateCredentials(login, password) == false) - { - return; - } + if (NotAutorized(login, password)) return; + if (SelfMessage(appId)) return; // do not process self-messages - if (ContinueRefreshingForRequest(appId, HttpRuntime.AppDomainAppId, NetworkHelper.MachineName) == false) return; - - //only execute distinct instructions - no sense in running the same one. + // only execute distinct instructions - no sense in running the same one more than once foreach (var instruction in instructions.Distinct()) { + var refresher = GetRefresher(instruction.RefresherId); switch (instruction.RefreshType) { - case RefreshInstruction.RefreshMethodType.RefreshAll: - RefreshAll(instruction.RefresherId); + case RefreshMethodType.RefreshAll: + refresher.RefreshAll(); break; - case RefreshInstruction.RefreshMethodType.RefreshByGuid: - RefreshByGuid(instruction.RefresherId, instruction.GuidId); + case RefreshMethodType.RefreshByGuid: + refresher.Refresh(instruction.GuidId); break; - case RefreshInstruction.RefreshMethodType.RefreshById: - RefreshById(instruction.RefresherId, instruction.IntId); + case RefreshMethodType.RefreshById: + refresher.Refresh(instruction.IntId); break; - case RefreshInstruction.RefreshMethodType.RefreshByIds: - RefreshByIds(instruction.RefresherId, instruction.JsonIds); + case RefreshMethodType.RefreshByIds: // not directly supported by ICacheRefresher + foreach (var id in JsonConvert.DeserializeObject(instruction.JsonIds)) + refresher.Refresh(id); break; - case RefreshInstruction.RefreshMethodType.RefreshByJson: - RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + case RefreshMethodType.RefreshByJson: + GetJsonRefresher(refresher).Refresh(instruction.JsonPayload); break; - case RefreshInstruction.RefreshMethodType.RemoveById: - RemoveById(instruction.RefresherId, instruction.IntId); + case RefreshMethodType.RemoveById: + refresher.Remove(instruction.IntId); break; + //case RefreshMethodType.RemoveByIds: // not directly supported by ICacheRefresher + // foreach (var id in JsonConvert.DeserializeObject(instruction.JsonIds)) + // refresher.Remove(id); + // break; } } } @@ -101,139 +106,61 @@ namespace umbraco.presentation.webservices [WebMethod] public void RefreshAll(Guid uniqueIdentifier, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshAll(uniqueIdentifier); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).RefreshAll(); } - private void RefreshAll(Guid uniqueIdentifier) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.RefreshAll(); - } - [WebMethod] public void RefreshByGuid(Guid uniqueIdentifier, Guid Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByGuid(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Refresh(Id); } - private void RefreshByGuid(Guid uniqueIdentifier, Guid Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Refresh(Id); - } - [WebMethod] public void RefreshById(Guid uniqueIdentifier, int Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshById(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Refresh(Id); } - private void RefreshById(Guid uniqueIdentifier, int Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Refresh(Id); - } - - /// - /// Refreshes objects for all Ids matched in the json string - /// - /// - /// A JSON Serialized string of ids to match - /// - /// [WebMethod] public void RefreshByIds(Guid uniqueIdentifier, string jsonIds, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByIds(uniqueIdentifier, jsonIds); - } + if (NotAutorized(Login, Password)) return; + var refresher = GetRefresher(uniqueIdentifier); + foreach (var id in JsonConvert.DeserializeObject(jsonIds)) + refresher.Refresh(id); } - private void RefreshByIds(Guid uniqueIdentifier, string jsonIds) - { - var serializer = new JavaScriptSerializer(); - var ids = serializer.Deserialize(jsonIds); - - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - foreach (var i in ids) - { - cr.Refresh(i); - } - } - - /// - /// Refreshes objects using the passed in Json payload, it will be up to the cache refreshers to deserialize - /// - /// - /// A custom JSON payload used by the cache refresher - /// - /// - /// - /// NOTE: the cache refresher defined by the ID MUST be of type IJsonCacheRefresher or an exception will be thrown - /// [WebMethod] public void RefreshByJson(Guid uniqueIdentifier, string jsonPayload, string Login, string Password) - { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RefreshByJson(uniqueIdentifier, jsonPayload); - } + { + if (NotAutorized(Login, Password)) return; + GetJsonRefresher(uniqueIdentifier).Refresh(jsonPayload); } - private void RefreshByJson(Guid uniqueIdentifier, string jsonPayload) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier) as IJsonCacheRefresher; - if (cr == null) - { - throw new InvalidOperationException("The cache refresher: " + uniqueIdentifier + " is not of type " + typeof(IJsonCacheRefresher)); - } - cr.Refresh(jsonPayload); - } - [WebMethod] public void RemoveById(Guid uniqueIdentifier, int Id, string Login, string Password) { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - RemoveById(uniqueIdentifier, Id); - } + if (NotAutorized(Login, Password)) return; + GetRefresher(uniqueIdentifier).Remove(Id); } - private void RemoveById(Guid uniqueIdentifier, int Id) - { - var cr = CacheRefreshersResolver.Current.GetById(uniqueIdentifier); - cr.Remove(Id); - } - [WebMethod] - public XmlDocument GetRefreshers(string Login, string Password) - { - if (BusinessLogic.User.validateCredentials(Login, Password)) - { - var xd = new XmlDocument(); - xd.LoadXml(""); - foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) - { - var n = xmlHelper.addTextNode(xd, "cacheRefresher", cr.Name); - n.Attributes.Append(xmlHelper.addAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); - xd.DocumentElement.AppendChild(n); - } - return xd; - - - } - return null; - } + public XmlDocument GetRefreshers(string Login, string Password) + { + if (NotAutorized(Login, Password)) return null; + var xd = new XmlDocument(); + xd.LoadXml(""); + foreach (var cr in CacheRefreshersResolver.Current.CacheRefreshers) + { + var n = xmlHelper.addTextNode(xd, "cacheRefresher", cr.Name); + n.Attributes.Append(xmlHelper.addAttribute(xd, "uniqueIdentifier", cr.UniqueIdentifier.ToString())); + xd.DocumentElement.AppendChild(n); + } + return xd; + } } } diff --git a/src/umbraco.interfaces/ICacheRefresher.cs b/src/umbraco.interfaces/ICacheRefresher.cs index 21f454ca66..d239e81fe6 100644 --- a/src/umbraco.interfaces/ICacheRefresher.cs +++ b/src/umbraco.interfaces/ICacheRefresher.cs @@ -15,6 +15,8 @@ namespace umbraco.interfaces void Refresh(int Id); void Remove(int Id); void Refresh(Guid Id); + + //void Notify(object payload); } } From 9213304161412bc4086adb24f2ae7fff4e440a01 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Mar 2015 10:45:43 +0100 Subject: [PATCH 030/249] Bugfix distributed cache --- .../Sync/DatabaseServerMessenger.cs | 33 ++++++++++--------- .../Sync/DatabaseServerRegistrar.cs | 2 +- src/Umbraco.Core/Sync/RefreshInstruction.cs | 12 +++++++ src/Umbraco.Core/Sync/ServerMessengerBase.cs | 6 ++-- .../BatchedDatabaseServerMessenger.cs | 6 +++- .../BatchedWebServiceServerMessenger.cs | 6 +++- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index ed472ea35a..956903d96d 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -98,8 +98,7 @@ namespace Umbraco.Core.Sync protected void Boot() { ReadLastSynced(); - if (_lastId < 0) // never synced before - Initialize(); + Initialize(); } /// @@ -107,24 +106,28 @@ namespace Umbraco.Core.Sync /// /// /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. + /// Callers MUST ensure thread-safety. /// private void Initialize() { - // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new - // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. - LogHelper.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); + if (_lastId < 0) // never synced before + { + // we haven't synced - in this case we aren't going to sync the whole thing, we will assume this is a new + // server and it will need to rebuild it's own caches, eg Lucene or the xml cache file. + LogHelper.Warn("No last synced Id found, this generally means this is a new server/install. The server will rebuild its caches and indexes and then adjust it's last synced id to the latest found in the database and will start maintaining cache updates based on that id"); - // go get the last id in the db and store it - // note: do it BEFORE initializing otherwise some instructions might get lost - // when doing it before, some instructions might run twice - not an issue - var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); - if (lastId > 0) - SaveLastSynced(lastId); + // go get the last id in the db and store it + // note: do it BEFORE initializing otherwise some instructions might get lost + // when doing it before, some instructions might run twice - not an issue + var lastId = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT MAX(id) FROM umbracoCacheInstruction"); + if (lastId > 0) + SaveLastSynced(lastId); - // execute initializing callbacks - if (_options.InitializingCallbacks != null) - foreach (var callback in _options.InitializingCallbacks) - callback(); + // execute initializing callbacks + if (_options.InitializingCallbacks != null) + foreach (var callback in _options.InitializingCallbacks) + callback(); + } _initialized = true; } diff --git a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs index e2f400ea71..8bf02b873d 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerRegistrar.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Sync /// /// A registrar that stores registered server nodes in the database. /// - internal sealed class DatabaseServerRegistrar : IServerRegistrar + public sealed class DatabaseServerRegistrar : IServerRegistrar { private readonly Lazy _registrationService; diff --git a/src/Umbraco.Core/Sync/RefreshInstruction.cs b/src/Umbraco.Core/Sync/RefreshInstruction.cs index a950b9bf78..fe37aa8fc7 100644 --- a/src/Umbraco.Core/Sync/RefreshInstruction.cs +++ b/src/Umbraco.Core/Sync/RefreshInstruction.cs @@ -15,6 +15,18 @@ namespace Umbraco.Core.Sync // but at the moment it is exposed in CacheRefresher webservice // so for the time being we keep it as-is for backward compatibility reasons + // need the public one so it can be de-serialized + // otherwise, should use GetInstructions(...) + public RefreshInstruction(Guid refresherId, RefreshMethodType refreshType, Guid guidId, int intId, string jsonIds, string jsonPayload) + { + RefresherId = refresherId; + RefreshType = refreshType; + GuidId = guidId; + IntId = intId; + JsonIds = jsonIds; + JsonPayload = jsonPayload; + } + private RefreshInstruction(ICacheRefresher refresher, RefreshMethodType refreshType) { RefresherId = refresher.UniqueIdentifier; diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs index fed29b85a8..2585922078 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -148,7 +148,7 @@ namespace Umbraco.Core.Sync { if (refresher == null) throw new ArgumentNullException("refresher"); - LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", refresher.GetType, () => messageType); @@ -200,7 +200,7 @@ namespace Umbraco.Core.Sync { if (refresher == null) throw new ArgumentNullException("refresher"); - LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", + LogHelper.Debug("Invoking refresher {0} on local server for message type {1}", refresher.GetType, () => messageType); @@ -238,7 +238,7 @@ namespace Umbraco.Core.Sync //{ // if (refresher == null) throw new ArgumentNullException("refresher"); - // LogHelper.Debug("Invoking refresher {0} on local server for message type Notify", + // LogHelper.Debug("Invoking refresher {0} on local server for message type Notify", // () => refresher.GetType()); // refresher.Notify(payload); diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index f98138c7f3..4bf4852f77 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -64,7 +64,11 @@ namespace Umbraco.Web { var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; if (httpContext == null) - throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + { + if (ensure) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + return null; + } var key = typeof (BatchedDatabaseServerMessenger).Name; diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs index 1789f92d9a..fca0b193db 100644 --- a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -34,7 +34,11 @@ namespace Umbraco.Web { var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; if (httpContext == null) - throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + { + if (ensure) + throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); + return null; + } var key = typeof(BatchedWebServiceServerMessenger).Name; From a7075422dcae3ba66f01f7cadd6a90f587b79048 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Mar 2015 12:28:45 +0100 Subject: [PATCH 031/249] Fix build --- src/umbraco.cms/businesslogic/Dictionary.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 94e5854243..0d34da4615 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -69,7 +69,6 @@ namespace umbraco.cms.businesslogic { throw new ArgumentException("No key " + key + " exists in dictionary"); } - var item = DictionaryItems[key]; } public DictionaryItem(Guid id) From 414374498f4fca51ac090a607b04a9c4bd0199d1 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 5 Mar 2015 19:49:36 +0100 Subject: [PATCH 032/249] Edit tests so they pass (ah well...) --- src/Umbraco.Tests/Plugins/PluginManagerTests.cs | 6 +++--- src/Umbraco.Tests/Plugins/TypeFinderTests.cs | 4 ++-- src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 959fbad11c..782626e912 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -268,7 +268,7 @@ namespace Umbraco.Tests.Plugins public void Resolves_Assigned_Mappers() { var foundTypes1 = _manager.ResolveAssignedMapperTypes(); - Assert.AreEqual(23, foundTypes1.Count()); + Assert.AreEqual(25, foundTypes1.Count()); } [Test] @@ -282,14 +282,14 @@ namespace Umbraco.Tests.Plugins public void Resolves_Attributed_Trees() { var trees = _manager.ResolveAttributedTrees(); - Assert.AreEqual(19, trees.Count()); + Assert.AreEqual(17, trees.Count()); } [Test] public void Resolves_Actions() { var actions = _manager.ResolveActions(); - Assert.AreEqual(36, actions.Count()); + Assert.AreEqual(37, actions.Count()); } [Test] diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index 74af8bf8db..576a21d5ac 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -79,8 +79,8 @@ namespace Umbraco.Tests.Plugins var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(5, typesFound.Count()); - Assert.AreEqual(5, originalTypesFound.Count()); + Assert.AreEqual(7, typesFound.Count()); + Assert.AreEqual(7, originalTypesFound.Count()); } [Test] diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index ab83294496..f5b041fc9f 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -452,7 +452,7 @@ namespace Umbraco.Tests.Scheduling Thread.Sleep(1000); Assert.IsTrue(runner.IsRunning); // waiting on delay Assert.AreEqual(0, MyDelayedRecurringTask.RunCount); - Thread.Sleep(1000); + Thread.Sleep(1200); Assert.AreEqual(1, MyDelayedRecurringTask.RunCount); Thread.Sleep(5000); Assert.GreaterOrEqual(MyDelayedRecurringTask.RunCount, 2); // keeps running, count >= 2 From 0ce503b369e4f792edec09f8d28aba9c480de688 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Mar 2015 13:34:09 +1100 Subject: [PATCH 033/249] Fixes: U4-6349 Optimizing indexes says that the index is not optimized --- .../Search/LuceneIndexerExtensions.cs | 43 ++++++++----------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs index 11615e1065..3cc7908dfd 100644 --- a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs +++ b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs @@ -13,23 +13,6 @@ namespace Umbraco.Web.Search /// internal static class ExamineExtensions { - public static LuceneSearcher GetSearcherForIndexer(this LuceneIndexer indexer) - { - var indexSet = indexer.IndexSetName; - var searcher = ExamineManager.Instance.SearchProviderCollection.OfType() - .FirstOrDefault(x => x.IndexSetName == indexSet); - if (searcher == null) - throw new InvalidOperationException("No searcher assigned to the index set " + indexer.IndexSetName); - return searcher; - } - - private static IndexReader GetIndexReaderForSearcher(this BaseLuceneSearcher searcher) - { - var indexSearcher = searcher.GetSearcher() as IndexSearcher; - if (indexSearcher == null) - throw new InvalidOperationException("The index searcher is not of type " + typeof(IndexSearcher) + " cannot execute this method"); - return indexSearcher.GetIndexReader(); - } /// /// Return the number of indexed documents in Lucene @@ -38,7 +21,10 @@ namespace Umbraco.Web.Search /// public static int GetIndexDocumentCount(this LuceneIndexer indexer) { - return indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().NumDocs(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDocs(); + } } /// @@ -48,7 +34,10 @@ namespace Umbraco.Web.Search /// public static int GetIndexFieldCount(this LuceneIndexer indexer) { - return indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().GetFieldNames(IndexReader.FieldOption.ALL).Count; + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + } } /// @@ -58,7 +47,10 @@ namespace Umbraco.Web.Search /// public static bool IsIndexOptimized(this LuceneIndexer indexer) { - return indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().IsOptimized(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.IsOptimized(); + } } /// @@ -70,9 +62,9 @@ namespace Umbraco.Web.Search /// If the index does not exist we'll consider it locked /// public static bool IsIndexLocked(this LuceneIndexer indexer) - { - return !indexer.IndexExists() - || IndexWriter.IsLocked(indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().Directory()); + { + return indexer.IndexExists() == false + || IndexWriter.IsLocked(indexer.GetLuceneDirectory()); } /// @@ -82,7 +74,10 @@ namespace Umbraco.Web.Search /// public static int GetDeletedDocumentsCount(this LuceneIndexer indexer) { - return indexer.GetSearcherForIndexer().GetIndexReaderForSearcher().NumDeletedDocs(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDeletedDocs(); + } } } } \ No newline at end of file From 7e9261d4bcd53032170d80a5456612e9d3ae4a91 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Mar 2015 13:36:52 +1100 Subject: [PATCH 034/249] Updates member indexer to consume less memory on rebuild, iterates over collection already in memory and just adds nodes as the are iterated, before was re-adding to memory in order to do counts, now we iteratively count. --- src/UmbracoExamine/UmbracoMemberIndexer.cs | 36 ++++++++++++---------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index d23ff26751..1fdfdb97b3 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -115,9 +115,10 @@ namespace UmbracoExamine return; //Re-index all members in batches of 5000 - IMember[] members; - const int pageSize = 5000; + int memberCount = 0; + const int pageSize = 1000; var pageIndex = 0; + var serializer = new EntityXmlSerializer(); if (IndexerData.IncludeNodeTypes.Any()) { @@ -127,10 +128,15 @@ namespace UmbracoExamine do { int total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + var members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, nodeType); + memberCount = 0; + foreach (var member in members) + { + AddNodesToIndex(new[] { serializer.Serialize(_dataTypeService, member) }, type); + memberCount++; + } pageIndex++; - } while (members.Length == pageSize); + } while (memberCount == pageSize); } } else @@ -139,22 +145,18 @@ namespace UmbracoExamine do { int total; - members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + var members = _memberService.GetAll(pageIndex, pageSize, out total); + memberCount = 0; + foreach (var member in members) + { + AddNodesToIndex(new[] {serializer.Serialize(_dataTypeService, member)}, type); + memberCount++; + } pageIndex++; - } while (members.Length == pageSize); + } while (memberCount == pageSize); } } - private IEnumerable GetSerializedMembers(IEnumerable members) - { - var serializer = new EntityXmlSerializer(); - foreach (var member in members) - { - yield return serializer.Serialize(_dataTypeService, member); - } - } - protected override XDocument GetXDocument(string xPath, string type) { throw new NotSupportedException(); From 0932c980e90ea66a083e3346577217bdf4cf1674 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Mar 2015 16:01:49 +1100 Subject: [PATCH 035/249] Fixes: U4-6307 Incorrect culture assigned to user (missing region code) --- src/Umbraco.Core/Models/UserExtensions.cs | 23 +++++-- .../Services/ILocalizedTextService.cs | 14 ++++ .../Services/LocalizedTextService.cs | 69 +++++++++++++++---- .../LocalizedTextServiceFileSources.cs | 61 +++++++++++++++- 4 files changed, 146 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 0621c83a72..99f72b8fb8 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using System.Linq; +using System.Threading; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -23,13 +24,21 @@ namespace Umbraco.Core.Models internal static CultureInfo GetUserCulture(string userLanguage, ILocalizedTextService textService) { - return textService.GetSupportedCultures() - .FirstOrDefault(culture => - //match on full name first - culture.Name.InvariantEquals(userLanguage.Replace("_", "-")) || - //then match on the 2 letter name - culture.TwoLetterISOLanguageName.InvariantEquals(userLanguage)); - } + try + { + var culture = CultureInfo.GetCultureInfo(userLanguage); + //TODO: This is a hack because we store the user language as 2 chars instead of the full culture + // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt + // to convert to a supported full culture + var result = textService.ConvertToSupportedCultureWithRegionCode(culture); + return result; + } + catch (CultureNotFoundException) + { + //return the default one + return CultureInfo.GetCultureInfo("en"); + } + } /// /// Checks if the user has access to the content item based on their start noe diff --git a/src/Umbraco.Core/Services/ILocalizedTextService.cs b/src/Umbraco.Core/Services/ILocalizedTextService.cs index de95f24efa..9875d63170 100644 --- a/src/Umbraco.Core/Services/ILocalizedTextService.cs +++ b/src/Umbraco.Core/Services/ILocalizedTextService.cs @@ -33,5 +33,19 @@ namespace Umbraco.Core.Services /// /// IEnumerable GetSupportedCultures(); + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned + /// + /// + /// + /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that + /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts + /// to resolve the full culture if possible. + /// + CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture); } } diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs index c9d05f3a3b..68c1c8d5e1 100644 --- a/src/Umbraco.Core/Services/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/LocalizedTextService.cs @@ -13,6 +13,7 @@ namespace Umbraco.Core.Services public class LocalizedTextService : ILocalizedTextService { + private readonly LocalizedTextServiceFileSources _fileSources; private readonly IDictionary>> _dictionarySource; private readonly IDictionary> _xmlSource; @@ -22,7 +23,8 @@ namespace Umbraco.Core.Services /// public LocalizedTextService(LocalizedTextServiceFileSources fileSources) { - _xmlSource = fileSources.GetXmlSources(); + if (fileSources == null) throw new ArgumentNullException("fileSources"); + _fileSources = fileSources; } /// @@ -49,6 +51,9 @@ namespace Umbraco.Core.Services { Mandate.ParameterNotNull(culture, "culture"); + //TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + //This is what the legacy ui service did if (string.IsNullOrEmpty(key)) return string.Empty; @@ -57,10 +62,14 @@ namespace Umbraco.Core.Services var area = keyParts.Length > 1 ? keyParts[0] : null; var alias = keyParts.Length > 1 ? keyParts[1] : keyParts[0]; - if (_xmlSource != null) + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + if (xmlSource != null) { - return GetFromXmlSource(culture, area, alias, tokens); - } + return GetFromXmlSource(xmlSource, culture, area, alias, tokens); + } else { return GetFromDictionarySource(culture, area, alias, tokens); @@ -75,18 +84,25 @@ namespace Umbraco.Core.Services { if (culture == null) throw new ArgumentNullException("culture"); + //TODO: Hack, see notes on ConvertToSupportedCultureWithRegionCode + culture = ConvertToSupportedCultureWithRegionCode(culture); + var result = new Dictionary(); - if (_xmlSource != null) + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + if (xmlSource != null) { - if (_xmlSource.ContainsKey(culture) == false) + if (xmlSource.ContainsKey(culture) == false) { LogHelper.Warn("The culture specified {0} was not found in any configured sources for this service", () => culture); return result; } //convert all areas + keys to a single key with a '/' - var areas = _xmlSource[culture].Value.XPathSelectElements("//area"); + var areas = xmlSource[culture].Value.XPathSelectElements("//area"); foreach (var area in areas) { var keys = area.XPathSelectElements("./key"); @@ -133,7 +149,36 @@ namespace Umbraco.Core.Services /// public IEnumerable GetSupportedCultures() { - return _xmlSource != null ? _xmlSource.Keys : _dictionarySource.Keys; + var xmlSource = _xmlSource ?? (_fileSources != null + ? _fileSources.GetXmlSources() + : null); + + return xmlSource != null ? xmlSource.Keys : _dictionarySource.Keys; + } + + /// + /// Tries to resolve a full 4 letter culture from a 2 letter culture name + /// + /// + /// The culture to determine if it is only a 2 letter culture, if so we'll try to convert it, otherwise it will just be returned + /// + /// + /// + /// TODO: This is just a hack due to the way we store the language files, they should be stored with 4 letters since that + /// is what they reference but they are stored with 2, further more our user's languages are stored with 2. So this attempts + /// to resolve the full culture if possible. + /// + /// This only works when this service is constructed with the LocalizedTextServiceFileSources + /// + public CultureInfo ConvertToSupportedCultureWithRegionCode(CultureInfo currentCulture) + { + if (currentCulture == null) throw new ArgumentNullException("currentCulture"); + + if (_fileSources == null) return currentCulture; + if (currentCulture.Name.Length > 2) return currentCulture; + + var attempt = _fileSources.TryConvert2LetterCultureTo4Letter(currentCulture.TwoLetterISOLanguageName); + return attempt ? attempt.Result : currentCulture; } private string GetFromDictionarySource(CultureInfo culture, string area, string key, IDictionary tokens) @@ -174,15 +219,15 @@ namespace Umbraco.Core.Services return "[" + key + "]"; } - private string GetFromXmlSource(CultureInfo culture, string area, string key, IDictionary tokens) + private static string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) { - if (_xmlSource.ContainsKey(culture) == false) + if (xmlSource.ContainsKey(culture) == false) { LogHelper.Warn("The culture specified {0} was not found in any configured sources for this service", () => culture); return "[" + key + "]"; } - var cultureSource = _xmlSource[culture].Value; + var cultureSource = xmlSource[culture].Value; var xpath = area.IsNullOrWhiteSpace() ? string.Format("//key [@alias = '{0}']", key) @@ -213,7 +258,7 @@ namespace Umbraco.Core.Services /// we support a dictionary which means in the future we can really have any sort of token system. /// Currently though, the token key's will need to be an integer and sequential - though we aren't going to throw exceptions if that is not the case. /// - internal string ParseTokens(string value, IDictionary tokens) + internal static string ParseTokens(string value, IDictionary tokens) { if (tokens == null || tokens.Any() == false) { diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 30e43a694c..be53d8d16b 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Xml; using System.Xml.Linq; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -16,6 +17,9 @@ namespace Umbraco.Core.Services private readonly IRuntimeCacheProvider _cache; private readonly DirectoryInfo _fileSourceFolder; + //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( + private readonly Dictionary _twoLetterCultureConverter = new Dictionary(); + public LocalizedTextServiceFileSources(IRuntimeCacheProvider cache, DirectoryInfo fileSourceFolder) { if (cache == null) throw new ArgumentNullException("cache"); @@ -46,10 +50,53 @@ namespace Umbraco.Core.Services { var localCopy = fileInfo; var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); - var culture = CultureInfo.GetCultureInfo(filename); + + //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct + // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we + // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that + // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) + // that any 4 letter file is named with the actual culture that it is! + CultureInfo culture = null; + if (filename.Length == 2) + { + //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't + //want to load in the entire doc into mem just to read a single value + using (var fs = fileInfo.OpenRead()) + using (var reader = XmlReader.Create(fs)) + { + if (reader.IsStartElement()) + { + if (reader.Name == "language") + { + if (reader.MoveToAttribute("culture")) + { + var cultureVal = reader.Value; + try + { + culture = CultureInfo.GetCultureInfo(cultureVal); + //add to the tracked dictionary + _twoLetterCultureConverter[filename] = culture; + } + catch (CultureNotFoundException) + { + LogHelper.Warn( + string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); + //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise + // an exception will be thrown. + } + } + } + } + } + } + if (culture == null) + { + culture = CultureInfo.GetCultureInfo(filename); + } + //get the lazy value from cache result.Add(culture, new Lazy(() => _cache.GetCacheItem( - string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.TwoLetterISOLanguageName), () => + string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.Name), () => { using (var fs = localCopy.OpenRead()) { @@ -59,5 +106,15 @@ namespace Umbraco.Core.Services } return result; } + + //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( + public Attempt TryConvert2LetterCultureTo4Letter(string twoLetterCulture) + { + if (twoLetterCulture.Length != 2) Attempt.Fail(); + + return _twoLetterCultureConverter.ContainsKey(twoLetterCulture) + ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture]) + : Attempt.Fail(); + } } } \ No newline at end of file From 9a5923771d4c9846c72dbff787fa30f549277458 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Mar 2015 16:05:37 +1100 Subject: [PATCH 036/249] fixes changing the underscore of a user language to culture. --- src/Umbraco.Core/Models/UserExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index 99f72b8fb8..bd670f3836 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Models { try { - var culture = CultureInfo.GetCultureInfo(userLanguage); + var culture = CultureInfo.GetCultureInfo(userLanguage.Replace("_", "-")); //TODO: This is a hack because we store the user language as 2 chars instead of the full culture // which is actually stored in the language files (which are also named with 2 chars!) so we need to attempt // to convert to a supported full culture From 017663833af13754e48de29353c095ae190cb178 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Mar 2015 16:54:55 +1100 Subject: [PATCH 037/249] Missing a fairly critical log entry --- src/Umbraco.Web/umbraco.presentation/content.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index e2773a7112..30bfd1c901 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1024,7 +1024,7 @@ namespace umbraco catch (Exception e) { // This is really bad, loading from cache file failed for some reason, now fallback to loading from database - Debug.WriteLine("Content file cache load failed: " + e); + LogHelper.Error("Content file cache load failed", e); DeleteXmlCache(); } } From 173392f26e4668c1d9cdffbf51e79b20c02ff1b1 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 6 Mar 2015 14:06:32 +0100 Subject: [PATCH 038/249] Fix environment issues with tests --- .../CoreXml/NavigableNavigatorTests.cs | 9 ------ .../Repositories/TemplateRepositoryTest.cs | 4 +-- src/Umbraco.Tests/StringNewlineExtensions.cs | 30 +++++++++++++++++++ .../Strings/StylesheetHelperTests.cs | 6 ++-- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 5 files changed, 36 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Tests/StringNewlineExtensions.cs diff --git a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs index 0f4b4a4f9e..41414cb81a 100644 --- a/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs +++ b/src/Umbraco.Tests/CoreXml/NavigableNavigatorTests.cs @@ -1162,13 +1162,4 @@ namespace Umbraco.Tests.CoreXml } #endregion - - static class StringCrLfExtensions - { - public static string Lf(this string s) - { - if (string.IsNullOrEmpty(s)) return s; - return s.Replace("\r", ""); // remove Cr - } - } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index a8cc8b41e3..7630e0aaf7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -102,7 +102,7 @@ namespace Umbraco.Tests.Persistence.Repositories -", template.Content); +".CrLf(), template.Content); } } @@ -131,7 +131,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(_masterPageFileSystem.FileExists("test2.master"), Is.True); Assert.AreEqual(@"<%@ Master Language=""C#"" MasterPageFile=""~/masterpages/test.master"" AutoEventWireup=""true"" %> -", template2.Content); +".CrLf(), template2.Content); } } diff --git a/src/Umbraco.Tests/StringNewlineExtensions.cs b/src/Umbraco.Tests/StringNewlineExtensions.cs new file mode 100644 index 0000000000..7e52e0f061 --- /dev/null +++ b/src/Umbraco.Tests/StringNewlineExtensions.cs @@ -0,0 +1,30 @@ +namespace Umbraco.Tests +{ + static class StringNewLineExtensions + { + /// + /// Ensures Lf only everywhere. + /// + /// The text to filter. + /// The filtered text. + public static string Lf(this string text) + { + if (string.IsNullOrEmpty(text)) return text; + text = text.Replace("\r", ""); // remove CR + return text; + } + + /// + /// Ensures CrLf everywhere. + /// + /// The text to filter. + /// The filtered text. + public static string CrLf(this string text) + { + if (string.IsNullOrEmpty(text)) return text; + text = text.Replace("\r", ""); // remove CR + text = text.Replace("\n", "\r\n"); // add CR everywhere + return text; + } + } +} diff --git a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs index 3f9f972b16..94c44e8114 100644 --- a/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs +++ b/src/Umbraco.Tests/Strings/StylesheetHelperTests.cs @@ -22,7 +22,7 @@ namespace Umbraco.Tests.Strings }); Assert.AreEqual(@"body {font-family:Arial;}/**umb_name:My new rule*/ -p{font-size:1em; color:blue;} /** umb_name: Test2 */ li {padding:0px;} table {margin:0;}", result); +p{font-size:1em; color:blue;} /** umb_name: Test2 */ li {padding:0px;} table {margin:0;}".CrLf(), result); } [Test] @@ -40,7 +40,7 @@ p{font-size:1em; color:blue;} /** umb_name: Test2 */ li {padding:0px;} table {m Assert.AreEqual(@"body {font-family:Arial;}/** Umb_Name: Test1 */ p { font-size: 1em; } /** umb_name: Test2 */ li {padding:0px;} table {margin:0;} /**umb_name:My new rule*/ -p{font-size:1em; color:blue;}", result); +p{font-size:1em; color:blue;}".CrLf(), result); } [Test] @@ -95,7 +95,7 @@ font-size: 1em; //Assert.IsTrue(results.First().RuleId.Value.Value.ToString() == file.Id.Value.Value + "/" + name); Assert.AreEqual(name, results.First().Name); Assert.AreEqual(selector, results.First().Selector); - Assert.AreEqual(styles, results.First().Styles); + Assert.AreEqual(styles.CrLf(), results.First().Styles); } // No Name: keyword diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index af7a5061b2..eb81fddb30 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -181,6 +181,7 @@ + From 5baaed19d03b6440473f8dcc05ad248accec0af9 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 6 Mar 2015 14:24:08 +0100 Subject: [PATCH 039/249] Fix tests --- .../Sync/WebServiceServerMessenger.cs | 4 ++-- src/Umbraco.Tests/Cache/CacheRefresherTests.cs | 17 ++++++++++++----- .../umbraco/webservices/CacheRefresher.asmx.cs | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs index c4b3c03d2f..1c63b591b7 100644 --- a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs +++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs @@ -126,8 +126,8 @@ namespace Umbraco.Core.Sync public static string GetServerHash(string machineName, string appDomainAppId) { var hasher = new HashCodeCombiner(); - hasher.AddCaseInsensitiveString(NetworkHelper.MachineName); - hasher.AddCaseInsensitiveString(System.Web.HttpRuntime.AppDomainAppId); + hasher.AddCaseInsensitiveString(appDomainAppId); + hasher.AddCaseInsensitiveString(machineName); return hasher.GetCombinedHashCode(); } diff --git a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs index 690ad80525..33dfd16743 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs +++ b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs @@ -1,3 +1,4 @@ +using System; using NUnit.Framework; using Umbraco.Core.Sync; using umbraco.presentation.webservices; @@ -9,16 +10,22 @@ namespace Umbraco.Tests.Cache { [TestCase("", "123456", "testmachine", true)] //empty hash will continue [TestCase("fffffff28449cf33", "123456", "testmachine", false)] //match, don't continue - [TestCase("fffffff28449cf33", "12345", "testmachine", true)] - [TestCase("fffffff28449cf33", "123456", "testmachin", true)] - [TestCase("fffffff28449cf3", "123456", "testmachine", true)] + [TestCase("fffffff28449cf33", "12345", "testmachine", true)] // no match, continue + [TestCase("fffffff28449cf33", "123456", "testmachin", true)] // same + [TestCase("fffffff28449cf3", "123456", "testmachine", true)] // same public void Continue_Refreshing_For_Request(string hash, string appDomainAppId, string machineName, bool expected) { if (expected) - Assert.AreEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); + Assert.IsTrue(Continue(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName))); else - Assert.AreNotEqual(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName)); + Assert.IsFalse(Continue(hash, WebServiceServerMessenger.GetServerHash(appDomainAppId, machineName))); } + // that's what CacheRefresher.asmx.cs does... + private bool Continue(string hash1, string hash2) + { + if (string.IsNullOrEmpty(hash1)) return true; + return hash1 != hash2; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs index 4186b2446a..5cfb8d3393 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/CacheRefresher.asmx.cs @@ -24,6 +24,7 @@ namespace umbraco.presentation.webservices // in which case we should ignore the message because it's been processed locally already internal static bool SelfMessage(string hash) { + if (string.IsNullOrEmpty(hash)) return false; // no hash = don't know = not self if (hash != WebServiceServerMessenger.GetCurrentServerHash()) return false; LogHelper.Debug( From 4544520b6673f92e08bac6f76299cae12b429827 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Mar 2015 15:06:32 +1100 Subject: [PATCH 040/249] Fixes: U4-6303 SaveAndPublishWithStatus error - updates to latest Examine with the fix. --- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 ++-- src/Umbraco.Tests/packages.config | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- src/Umbraco.Web.UI/packages.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- src/Umbraco.Web/packages.config | 2 +- src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj | 4 ++-- src/UmbracoExamine.PDF/packages.config | 2 +- src/UmbracoExamine/UmbracoExamine.csproj | 4 ++-- src/UmbracoExamine/packages.config | 2 +- src/umbraco.MacroEngines/packages.config | 2 +- src/umbraco.MacroEngines/umbraco.MacroEngines.csproj | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index ff8b53717f..1effed5ac0 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -58,9 +58,9 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - + False - ..\packages\Examine.0.1.60.2941\lib\Examine.dll + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index 9da519e0ef..f755c1958f 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 3635e5174d..2015d0f2f6 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -127,9 +127,9 @@ False ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.60.2941\lib\Examine.dll - True + + False + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index e5c41208c3..bd2ee17ca4 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -4,7 +4,7 @@ - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index fd2a1561e1..4421f6b251 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -115,9 +115,9 @@ ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - + False - ..\packages\Examine.0.1.60.2941\lib\Examine.dll + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 6cca745cc3..f11d847bbf 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -3,7 +3,7 @@ - + diff --git a/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj b/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj index 0222d6976f..adc9b175fc 100644 --- a/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj +++ b/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj @@ -46,9 +46,9 @@ false - + False - ..\packages\Examine.0.1.60.2941\lib\Examine.dll + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False diff --git a/src/UmbracoExamine.PDF/packages.config b/src/UmbracoExamine.PDF/packages.config index df682e6454..72c8a05f8b 100644 --- a/src/UmbracoExamine.PDF/packages.config +++ b/src/UmbracoExamine.PDF/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index a9ae64e70d..6e50ec5a4b 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -82,9 +82,9 @@ ..\Solution Items\TheFARM-Public.snk - + False - ..\packages\Examine.0.1.60.2941\lib\Examine.dll + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False diff --git a/src/UmbracoExamine/packages.config b/src/UmbracoExamine/packages.config index f30abc56f9..47328cd5f6 100644 --- a/src/UmbracoExamine/packages.config +++ b/src/UmbracoExamine/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/packages.config b/src/umbraco.MacroEngines/packages.config index 831117f5e5..af1c82e11d 100644 --- a/src/umbraco.MacroEngines/packages.config +++ b/src/umbraco.MacroEngines/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj index 456b1e5662..0aaf1b85a8 100644 --- a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj +++ b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj @@ -45,9 +45,9 @@ false - + False - ..\packages\Examine.0.1.60.2941\lib\Examine.dll + ..\packages\Examine.0.1.61.2941\lib\Examine.dll False From b3f6f4883472971178c6c753fc572ceba7a20b09 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Mar 2015 16:42:30 +1100 Subject: [PATCH 041/249] fixes ugly null check on ContentTypeSort comparison --- src/Umbraco.Core/Models/ContentTypeSort.cs | 25 +++++++++++++++++-- .../Repositories/ContentTypeBaseRepository.cs | 2 +- .../ContentTypeDefinitionFactory.cs | 6 ++--- .../Repositories/ContentRepositoryTest.cs | 7 +----- .../Repositories/ContentTypeRepositoryTest.cs | 14 ++--------- .../Services/PerformanceTests.cs | 7 +----- .../controls/ContentTypeControlNew.ascx.cs | 2 +- src/umbraco.cms/businesslogic/ContentType.cs | 2 +- 8 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index 5aa81d9db0..40beb70939 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -8,8 +8,16 @@ namespace Umbraco.Core.Models /// public class ContentTypeSort : IValueObject, IDeepCloneable { + [Obsolete("This parameterless constructor should never be used")] public ContentTypeSort() { + + } + + public ContentTypeSort(Lazy id, int sortOrder) + { + Id = id; + SortOrder = sortOrder; } public ContentTypeSort(Lazy id, int sortOrder, string @alias) @@ -45,9 +53,16 @@ namespace Umbraco.Core.Models protected bool Equals(ContentTypeSort other) { - return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); + return Id.Equals(other.Id) && string.Equals(Alias, other.Alias); } + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; @@ -56,11 +71,17 @@ namespace Umbraco.Core.Models return Equals((ContentTypeSort) obj); } + /// + /// Serves as a hash function for a particular type. + /// + /// + /// A hash code for the current . + /// public override int GetHashCode() { unchecked { - return (Id.GetHashCode()*397) ^ Alias.GetHashCode(); + return (Id.Value.GetHashCode()*397) ^ (Alias != null ? Alias.GetHashCode() : 0); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 863f9ebeb7..5477e5dd96 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -414,7 +414,7 @@ AND umbracoNode.id <> @id", .Where(x => x.Id == id); var allowedContentTypeDtos = Database.Fetch(sql); - return allowedContentTypeDtos.Select(x => new ContentTypeSort { Id = new Lazy(() => x.AllowedId), Alias = x.ContentTypeDto.Alias, SortOrder = x.SortOrder }).ToList(); + return allowedContentTypeDtos.Select(x => new ContentTypeSort(new Lazy(() => x.AllowedId), x.SortOrder, x.ContentTypeDto.Alias)).ToList(); } protected PropertyGroupCollection GetPropertyGroupCollection(int id, DateTime createDate, DateTime updateDate) diff --git a/src/Umbraco.Tests/CodeFirst/Definitions/ContentTypeDefinitionFactory.cs b/src/Umbraco.Tests/CodeFirst/Definitions/ContentTypeDefinitionFactory.cs index f1b98c53e6..43672de279 100644 --- a/src/Umbraco.Tests/CodeFirst/Definitions/ContentTypeDefinitionFactory.cs +++ b/src/Umbraco.Tests/CodeFirst/Definitions/ContentTypeDefinitionFactory.cs @@ -347,12 +347,10 @@ namespace Umbraco.Tests.CodeFirst.Definitions { if(type == currentType) continue;//If the referenced type is equal to the current type we skip it to avoid a circular dependency - var contentTypeSort = new ContentTypeSort(); + var isResolved = _contentTypeCache.ContainsKey(type.FullName); var lazy = isResolved ? _contentTypeCache[type.FullName].ContentType : GetContentTypeDefinition(type); - contentTypeSort.Id = new Lazy(() => lazy.Value.Id); - contentTypeSort.Alias = lazy.Value.Alias; - contentTypeSort.SortOrder = order; + var contentTypeSort = new ContentTypeSort(new Lazy(() => lazy.Value.Id), order, lazy.Value.Alias); contentTypeSorts.Add(contentTypeSort); order++; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 86c26fd0e5..f55b185d8b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -172,12 +172,7 @@ namespace Umbraco.Tests.Persistence.Repositories var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage"); contentType.AllowedContentTypes = new List { - new ContentTypeSort - { - Alias = contentType.Alias, - Id = new Lazy(() => contentType.Id), - SortOrder = 0 - } + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias) }; var parentPage = MockedContent.CreateSimpleContent(contentType); contentTypeRepository.AddOrUpdate(contentType); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 5d94e604a4..81fae8137c 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -386,18 +386,8 @@ namespace Umbraco.Tests.Persistence.Repositories var contentType = repository.Get(NodeDto.NodeIdSeed); contentType.AllowedContentTypes = new List { - new ContentTypeSort - { - Alias = subpageContentType.Alias, - Id = new Lazy(() => subpageContentType.Id), - SortOrder = 0 - }, - new ContentTypeSort - { - Alias = simpleSubpageContentType.Alias, - Id = new Lazy(() => simpleSubpageContentType.Id), - SortOrder = 1 - } + new ContentTypeSort(new Lazy(() => subpageContentType.Id), 0, subpageContentType.Alias), + new ContentTypeSort(new Lazy(() => simpleSubpageContentType.Id), 1, simpleSubpageContentType.Alias) }; repository.AddOrUpdate(contentType); unitOfWork.Commit(); diff --git a/src/Umbraco.Tests/Services/PerformanceTests.cs b/src/Umbraco.Tests/Services/PerformanceTests.cs index 7e754b6635..d128107d37 100644 --- a/src/Umbraco.Tests/Services/PerformanceTests.cs +++ b/src/Umbraco.Tests/Services/PerformanceTests.cs @@ -239,12 +239,7 @@ namespace Umbraco.Tests.Services ServiceContext.ContentTypeService.Save(contentType1); contentType1.AllowedContentTypes = new List { - new ContentTypeSort - { - Alias = contentType1.Alias, - Id = new Lazy(() => contentType1.Id), - SortOrder = 0 - } + new ContentTypeSort(new Lazy(() => contentType1.Id), 0, contentType1.Alias) }; var result = new List(); ServiceContext.ContentTypeService.Save(contentType1); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs index 6ad092c2b6..3bd510690e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs @@ -321,7 +321,7 @@ namespace umbraco.controls int i = 0; var ids = SaveAllowedChildTypes(); - _contentType.ContentTypeItem.AllowedContentTypes = ids.Select(x => new ContentTypeSort {Id = new Lazy(() => x), SortOrder = i++}); + _contentType.ContentTypeItem.AllowedContentTypes = ids.Select(x => new ContentTypeSort(new Lazy(() => x), i++)); // figure out whether compositions are locked var allContentTypes = Request.Path.ToLowerInvariant().Contains("editmediatype.aspx") diff --git a/src/umbraco.cms/businesslogic/ContentType.cs b/src/umbraco.cms/businesslogic/ContentType.cs index 01a63f0625..a9f11321fe 100644 --- a/src/umbraco.cms/businesslogic/ContentType.cs +++ b/src/umbraco.cms/businesslogic/ContentType.cs @@ -880,7 +880,7 @@ namespace umbraco.cms.businesslogic foreach (var i in value) { int id = i; - list.Add(new ContentTypeSort { Id = new Lazy(() => id), SortOrder = sort }); + list.Add(new ContentTypeSort(new Lazy(() => id), sort)); sort++; } From 2834ccdc2b057619178b3df49aa05ae938faca15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Mar 2015 17:31:29 +1100 Subject: [PATCH 042/249] Fixes: U4-6344 TinyMCE RTE style_formats configuration changes --- .../tinymce/plugins/codemirror/source.html | 2 +- .../propertyeditors/rte/rte.controller.js | 27 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/codemirror/source.html b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/codemirror/source.html index d8c72044eb..359066d688 100755 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/codemirror/source.html +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/codemirror/source.html @@ -31,7 +31,7 @@ function inArray(key, arr) tinymce = parent.tinymce; editor = tinymce.activeEditor; - var i, userSettings = editor.settings.codemirror ? JSON.parse(editor.settings.codemirror) : {}; + var i, userSettings = editor.settings.codemirror ? (typeof editor.settings.codemirror === 'object') ? editor.settings.codemirror : JSON.parse(editor.settings.codemirror) : {}; CMsettings = { path: userSettings.path || 'CodeMirror', indentOnInit: userSettings.indentOnInit || false, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 4237037070..3ce153b893 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -1,7 +1,7 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.RTEController", function ($rootScope, $scope, $q, dialogService, $log, imageHelper, assetsService, $timeout, tinyMceService, angularHelper, stylesheetResource) { - + $scope.isLoading = true; //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias @@ -125,6 +125,31 @@ angular.module("umbraco") if (tinyMceConfig.customConfig) { + + //if there is some custom config, we need to see if the string value of each item might actually be json and if so, we need to + // convert it to json instead of having it as a string since this is what tinymce requires + for (var i in tinyMceConfig.customConfig) { + var val = tinyMceConfig.customConfig[i]; + if (val) { + val = val.toString().trim(); + if (val.detectIsJson()) { + try { + tinyMceConfig.customConfig[i] = JSON.parse(val); + //now we need to check if this custom config key is defined in our baseline, if it is we don't want to + //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise + //if it's an object it will overwrite the baseline + if (angular.isArray(baseLineConfigObj[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + //concat it and below this concat'd array will overwrite the baseline in angular.extend + tinyMceConfig.customConfig[i] = baseLineConfigObj[i].concat(tinyMceConfig.customConfig[i]); + } + } + catch (e) { + //cannot parse, we'll just leave it + } + } + } + } + angular.extend(baseLineConfigObj, tinyMceConfig.customConfig); } From dd477a0cbe3bbdce77f1a09870d61788e430977f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Mar 2015 17:37:25 +1100 Subject: [PATCH 043/249] whoops, fixes comparison on lazy val --- src/Umbraco.Core/Models/ContentTypeSort.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index 40beb70939..e80d547797 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -53,7 +53,7 @@ namespace Umbraco.Core.Models protected bool Equals(ContentTypeSort other) { - return Id.Equals(other.Id) && string.Equals(Alias, other.Alias); + return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); } /// From fe40b8fe999ad5b3494e6d30b2766d0998c165d6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Mar 2015 17:53:58 +1100 Subject: [PATCH 044/249] ok, fixes the null check for real this time. --- src/Umbraco.Core/Models/ContentTypeSort.cs | 27 +++++++------------ .../controls/ContentTypeControlNew.ascx.cs | 2 +- src/umbraco.cms/businesslogic/ContentType.cs | 2 +- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index e80d547797..40d0522b52 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -11,12 +11,14 @@ namespace Umbraco.Core.Models [Obsolete("This parameterless constructor should never be used")] public ContentTypeSort() { - } - public ContentTypeSort(Lazy id, int sortOrder) + /// + /// Initializes a new instance of the class. + /// + public ContentTypeSort(int id, int sortOrder) { - Id = id; + Id = new Lazy(() => id); SortOrder = sortOrder; } @@ -53,16 +55,9 @@ namespace Umbraco.Core.Models protected bool Equals(ContentTypeSort other) { - return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); + return Id.Value.Equals(other.Id.Value) && string.Equals(Alias, other.Alias); } - /// - /// Determines whether the specified is equal to the current . - /// - /// - /// true if the specified object is equal to the current object; otherwise, false. - /// - /// The object to compare with the current object. public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; @@ -71,17 +66,13 @@ namespace Umbraco.Core.Models return Equals((ContentTypeSort) obj); } - /// - /// Serves as a hash function for a particular type. - /// - /// - /// A hash code for the current . - /// public override int GetHashCode() { unchecked { - return (Id.Value.GetHashCode()*397) ^ (Alias != null ? Alias.GetHashCode() : 0); + //The hash code will just be the alias if one is assigned, otherwise it will be the hash code of the Id. + //In some cases the alias can be null of the non lazy ctor is used, in that case, the lazy Id will already have a value created. + return Alias != null ? Alias.GetHashCode() : (Id.Value.GetHashCode() * 397); } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs index 3bd510690e..111750e429 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs @@ -321,7 +321,7 @@ namespace umbraco.controls int i = 0; var ids = SaveAllowedChildTypes(); - _contentType.ContentTypeItem.AllowedContentTypes = ids.Select(x => new ContentTypeSort(new Lazy(() => x), i++)); + _contentType.ContentTypeItem.AllowedContentTypes = ids.Select(x => new ContentTypeSort(x, i++)); // figure out whether compositions are locked var allContentTypes = Request.Path.ToLowerInvariant().Contains("editmediatype.aspx") diff --git a/src/umbraco.cms/businesslogic/ContentType.cs b/src/umbraco.cms/businesslogic/ContentType.cs index a9f11321fe..ff6e40fe50 100644 --- a/src/umbraco.cms/businesslogic/ContentType.cs +++ b/src/umbraco.cms/businesslogic/ContentType.cs @@ -880,7 +880,7 @@ namespace umbraco.cms.businesslogic foreach (var i in value) { int id = i; - list.Add(new ContentTypeSort(new Lazy(() => id), sort)); + list.Add(new ContentTypeSort(id, sort)); sort++; } From d2083673bed532e4241145988fa323d9bf6d35b9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Mar 2015 13:02:34 +1100 Subject: [PATCH 045/249] Fixes: U4-3420 Firefox v25.0 - Right click context menu doesn't work - this felt like being a web dev 10 years ago when you had to code totally differently for each browser. Firefox unfortunatley treats 'a' tags differently than anything else and combined with angular saying you can have empty 'href' tags doesn't work. In the end, the fix was easy, just remove the empty (supposed to be supported) href attributes from the 'a' tags in the tree, then the contextmenu starts working in firefox and the events can be canceled as per normal. This commit also moves the angular localization library to use bower. --- src/Umbraco.Web.UI.Client/bower.json | 6 +- .../lib/angular/tmhDynamicLocale.js | 186 ------------------ .../common/directives/umbtree.directive.js | 2 +- .../directives/umbtreeitem.directive.js | 5 +- src/Umbraco.Web.UI.Client/src/loader.js | 2 +- src/Umbraco.Web/UI/JavaScript/JsInitialize.js | 2 +- 6 files changed, 11 insertions(+), 192 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/lib/angular/tmhDynamicLocale.js diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 4f7a01e20e..e8ad94653f 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -21,7 +21,8 @@ "rgrove-lazyload": "*", "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", - "jquery-ui": "1.10.3" + "jquery-ui": "1.10.3", + "angular-dynamic-locale": "~0.1.27" }, "exportsOverride": { "rgrove-lazyload": { @@ -33,6 +34,9 @@ "underscore": { "": "underscore-min.{js,map}" }, + "angular-dynamic-locale": { + "": "tmhDynamicLocale.min.{js,js.map}" + }, "jquery": { "": "jquery.min.{js,map}" }, diff --git a/src/Umbraco.Web.UI.Client/lib/angular/tmhDynamicLocale.js b/src/Umbraco.Web.UI.Client/lib/angular/tmhDynamicLocale.js deleted file mode 100644 index 325475f951..0000000000 --- a/src/Umbraco.Web.UI.Client/lib/angular/tmhDynamicLocale.js +++ /dev/null @@ -1,186 +0,0 @@ -( function(window) { -'use strict'; -angular.module('tmh.dynamicLocale', []).provider('tmhDynamicLocale', function() { - - var defaultLocale, - localeLocationPattern = 'angular/i18n/angular-locale_{{locale}}.js', - storageFactory = 'tmhDynamicLocaleStorageCache', - storage, - storeKey = 'tmhDynamicLocale.locale', - promiseCache = {}, - activeLocale; - - /** - * Loads a script asynchronously - * - * @param {string} url The url for the script - @ @param {function) callback A function to be called once the script is loaded - */ - function loadScript(url, callback, errorCallback, $timeout) { - var script = document.createElement('script'), - body = document.getElementsByTagName('body')[0], - removed = false; - - script.type = 'text/javascript'; - if (script.readyState) { // IE - script.onreadystatechange = function () { - if (script.readyState === 'complete' || - script.readyState === 'loaded') { - script.onreadystatechange = null; - $timeout( - function () { - if (removed) return; - removed = true; - body.removeChild(script); - callback(); - }, 30, false); - } - }; - } else { // Others - script.onload = function () { - if (removed) return; - removed = true; - body.removeChild(script); - callback(); - }; - script.onerror = function () { - if (removed) return; - removed = true; - body.removeChild(script); - errorCallback(); - }; - } - script.src = url; - script.async = false; - body.appendChild(script); - } - - /** - * Loads a locale and replaces the properties from the current locale with the new locale information - * - * @param localeUrl The path to the new locale - * @param $locale The locale at the curent scope - */ - function loadLocale(localeUrl, $locale, localeId, $rootScope, $q, localeCache, $timeout) { - - function overrideValues(oldObject, newObject) { - if (activeLocale !== localeId) { - return; - } - angular.forEach(oldObject, function(value, key) { - if (!newObject[key]) { - delete oldObject[key]; - } else if (angular.isArray(newObject[key])) { - oldObject[key].length = newObject[key].length; - } - }); - angular.forEach(newObject, function(value, key) { - if (angular.isArray(newObject[key]) || angular.isObject(newObject[key])) { - if (!oldObject[key]) { - oldObject[key] = angular.isArray(newObject[key]) ? [] : {}; - } - overrideValues(oldObject[key], newObject[key]); - } else { - oldObject[key] = newObject[key]; - } - }); - } - - - if (promiseCache[localeId]) return promiseCache[localeId]; - - var cachedLocale, - deferred = $q.defer(); - if (localeId === activeLocale) { - deferred.resolve($locale); - } else if ((cachedLocale = localeCache.get(localeId))) { - activeLocale = localeId; - $rootScope.$evalAsync(function() { - overrideValues($locale, cachedLocale); - $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale); - storage.put(storeKey, localeId); - deferred.resolve($locale); - }); - } else { - activeLocale = localeId; - promiseCache[localeId] = deferred.promise; - loadScript(localeUrl, function () { - // Create a new injector with the new locale - var localInjector = angular.injector(['ngLocale']), - externalLocale = localInjector.get('$locale'); - - overrideValues($locale, externalLocale); - localeCache.put(localeId, externalLocale); - delete promiseCache[localeId]; - - $rootScope.$apply(function () { - $rootScope.$broadcast('$localeChangeSuccess', localeId, $locale); - storage.put(storeKey, localeId); - deferred.resolve($locale); - }); - }, function () { - delete promiseCache[localeId]; - - $rootScope.$apply(function () { - $rootScope.$broadcast('$localeChangeError', localeId); - deferred.reject(localeId); - }); - }, $timeout); - } - return deferred.promise; - } - - this.localeLocationPattern = function(value) { - if (value) { - localeLocationPattern = value; - return this; - } else { - return localeLocationPattern; - } - }; - - this.useStorage = function(storageName) { - storageFactory = storageName; - }; - - this.useCookieStorage = function() { - this.useStorage('$cookieStore'); - }; - - this.defaultLocale = function (value) { - defaultLocale = value; - }; - - this.$get = ['$rootScope', '$injector', '$interpolate', '$locale', '$q', 'tmhDynamicLocaleCache', '$timeout', function($rootScope, $injector, interpolate, locale, $q, tmhDynamicLocaleCache, $timeout) { - var localeLocation = interpolate(localeLocationPattern); - - storage = $injector.get(storageFactory); - $rootScope.$evalAsync(function () { - var initialLocale; - if ((initialLocale = (storage.get(storeKey) || defaultLocale))) { - loadLocale(localeLocation({locale: initialLocale}), locale, initialLocale, $rootScope, $q, tmhDynamicLocaleCache, $timeout); - } - }); - return { - /** - * @ngdoc method - * @description - * @param {string=} value Sets the locale to the new locale. Changing the locale will trigger - * a background task that will retrieve the new locale and configure the current $locale - * instance with the information from the new locale - */ - set: function(value) { - return loadLocale(localeLocation({locale: value}), locale, value, $rootScope, $q, tmhDynamicLocaleCache, $timeout); - } - }; - }]; -}).provider('tmhDynamicLocaleCache', function() { - this.$get = ['$cacheFactory', function($cacheFactory) { - return $cacheFactory('tmh.dynamicLocales'); - }]; -}).provider('tmhDynamicLocaleStorageCache', function() { - this.$get = ['$cacheFactory', function($cacheFactory) { - return $cacheFactory('tmh.dynamicLocales.store'); - }]; -}).run(['tmhDynamicLocale', angular.noop]); -}(window) ); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js index aabf19268c..2aaf66c81a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtree.directive.js @@ -33,7 +33,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat '
' + '' + '{{tree.name}}
' + - '' + + '' + ''; template += '
    ' + '' + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js index a202a2c7a7..4f4971f312 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbtreeitem.directive.js @@ -41,9 +41,9 @@ angular.module("umbraco.directives") '' + '' + '' + - '' + + '' + //NOTE: These are the 'option' elipses - '' + + '' + '
    ' + '' + '', @@ -59,6 +59,7 @@ angular.module("umbraco.directives") // Helper function to emit tree events function emitEvent(eventName, args) { + if (scope.eventhandler) { $(scope.eventhandler).trigger(eventName, args); } diff --git a/src/Umbraco.Web.UI.Client/src/loader.js b/src/Umbraco.Web.UI.Client/src/loader.js index 3624a07c3c..32def61618 100644 --- a/src/Umbraco.Web.UI.Client/src/loader.js +++ b/src/Umbraco.Web.UI.Client/src/loader.js @@ -12,7 +12,7 @@ LazyLoad.js( 'lib/angular/angular-ui-sortable.js', - 'lib/angular/tmhDynamicLocale.js', + 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', /* App-wide file-upload helper */ 'lib/jquery-file-upload/jquery.fileupload.js', diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 7f3d7f321d..3ff9884ba7 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -11,7 +11,7 @@ 'lib/angular/angular-ui-sortable.js', - 'lib/angular/tmhDynamicLocale.js', + 'lib/angular-dynamic-locale/tmhDynamicLocale.min.js', 'lib/blueimp-load-image/load-image.all.min.js', 'lib/jquery-file-upload/jquery.fileupload.js', From b52f234c0295cd665b3e55331fa04e103fc7387a Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 12 Mar 2015 15:01:25 +0100 Subject: [PATCH 046/249] U4-3753 - refactor --- src/Umbraco.Web/Models/ContentExtensions.cs | 29 ++++++++++++++++--- src/Umbraco.Web/PublishedContentExtensions.cs | 26 ++++------------- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 85f1a3033d..dc12a90250 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Routing; namespace Umbraco.Web.Models @@ -18,10 +19,30 @@ namespace Umbraco.Web.Models /// The culture that would be selected to render the content. public static CultureInfo GetCulture(this IContent content, Uri current = null) { - var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + return GetCulture(UmbracoContext.Current, + ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + content.Id, content.Path, + current); + } + + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// An instance. + /// An implementation. + /// An implementation. + /// The content identifier. + /// The content path. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(UmbracoContext umbracoContext, IDomainService domainService, ILocalizationService localizationService, + int contentId, string contentPath, Uri current) + { + var route = umbracoContext.ContentCache.GetRouteById(contentId); // cached var pos = route.IndexOf('/'); - var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + var domainHelper = new DomainHelper(domainService); var domain = pos == 0 ? null @@ -29,11 +50,11 @@ namespace Umbraco.Web.Models if (domain == null) { - var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + var defaultLanguage = localizationService.GetAllLanguages().FirstOrDefault(); return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); } - var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContent.Id); return wcDomain == null ? new CultureInfo(domain.Language.IsoCode) : new CultureInfo(wcDomain.Language.IsoCode); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 923edf50c2..7302913d34 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -7,6 +7,7 @@ using System.Web; using Examine.LuceneEngine.SearchCriteria; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Core; using Umbraco.Web.Routing; @@ -1896,27 +1897,12 @@ namespace Umbraco.Web /// The culture that would be selected to render the content. public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) { - var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached - var pos = route.IndexOf('/'); - - var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); - - var domain = pos == 0 - ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; - - if (domain == null) - { - var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); - return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); - } - - var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); - return wcDomain == null - ? new CultureInfo(domain.Language.IsoCode) - : new CultureInfo(wcDomain.Language.IsoCode); + return Models.ContentExtensions.GetCulture(UmbracoContext.Current, + ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + content.Id, content.Path, + current); } - + #endregion } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1ce693b27a..a5084b6a31 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -281,6 +281,7 @@ + From 20b21c86eab1cd743725b1b5131e674d37f6e19b Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 3 Mar 2015 13:03:11 +0100 Subject: [PATCH 047/249] U4-3753 - add a way to get the rendering culture of a content --- src/Umbraco.Web/Models/ContentExtensions.cs | 42 +++++++++++++++++++ src/Umbraco.Web/PublishedContentExtensions.cs | 36 ++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Umbraco.Web/Models/ContentExtensions.cs diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs new file mode 100644 index 0000000000..85f1a3033d --- /dev/null +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Globalization; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Models +{ + public static class ContentExtensions + { + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// The content. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(this IContent content, Uri current = null) + { + var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + var pos = route.IndexOf('/'); + + var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + + var domain = pos == 0 + ? null + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + + if (domain == null) + { + var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); + } + + var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + return wcDomain == null + ? new CultureInfo(domain.Language.IsoCode) + : new CultureInfo(wcDomain.Language.IsoCode); + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d7a19288fc..923edf50c2 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data; +using System.Globalization; using System.Linq; using System.Web; using Examine.LuceneEngine.SearchCriteria; @@ -8,6 +9,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Models; using Umbraco.Core; +using Umbraco.Web.Routing; using ContentType = umbraco.cms.businesslogic.ContentType; namespace Umbraco.Web @@ -1882,5 +1884,39 @@ namespace Umbraco.Web } #endregion + + #region Culture + + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// The content. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) + { + var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + var pos = route.IndexOf('/'); + + var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + + var domain = pos == 0 + ? null + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + + if (domain == null) + { + var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); + } + + var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + return wcDomain == null + ? new CultureInfo(domain.Language.IsoCode) + : new CultureInfo(wcDomain.Language.IsoCode); + } + + #endregion } } From 420c334af47cf342808677359b0409c53aac2f59 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 12 Mar 2015 15:01:25 +0100 Subject: [PATCH 048/249] U4-3753 - refactor Conflicts: src/Umbraco.Web/Umbraco.Web.csproj --- src/Umbraco.Web/Models/ContentExtensions.cs | 29 ++++++++++++++++--- src/Umbraco.Web/PublishedContentExtensions.cs | 26 ++++------------- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 85f1a3033d..dc12a90250 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Routing; namespace Umbraco.Web.Models @@ -18,10 +19,30 @@ namespace Umbraco.Web.Models /// The culture that would be selected to render the content. public static CultureInfo GetCulture(this IContent content, Uri current = null) { - var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached + return GetCulture(UmbracoContext.Current, + ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + content.Id, content.Path, + current); + } + + /// + /// Gets the culture that would be selected to render a specified content, + /// within the context of a specified current request. + /// + /// An instance. + /// An implementation. + /// An implementation. + /// The content identifier. + /// The content path. + /// The request Uri. + /// The culture that would be selected to render the content. + public static CultureInfo GetCulture(UmbracoContext umbracoContext, IDomainService domainService, ILocalizationService localizationService, + int contentId, string contentPath, Uri current) + { + var route = umbracoContext.ContentCache.GetRouteById(contentId); // cached var pos = route.IndexOf('/'); - var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); + var domainHelper = new DomainHelper(domainService); var domain = pos == 0 ? null @@ -29,11 +50,11 @@ namespace Umbraco.Web.Models if (domain == null) { - var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); + var defaultLanguage = localizationService.GetAllLanguages().FirstOrDefault(); return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); } - var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); + var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContent.Id); return wcDomain == null ? new CultureInfo(domain.Language.IsoCode) : new CultureInfo(wcDomain.Language.IsoCode); diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 923edf50c2..7302913d34 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -7,6 +7,7 @@ using System.Web; using Examine.LuceneEngine.SearchCriteria; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Core; using Umbraco.Web.Routing; @@ -1896,27 +1897,12 @@ namespace Umbraco.Web /// The culture that would be selected to render the content. public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) { - var route = UmbracoContext.Current.ContentCache.GetRouteById(content.Id); // cached - var pos = route.IndexOf('/'); - - var domainHelper = new DomainHelper(ApplicationContext.Current.Services.DomainService); - - var domain = pos == 0 - ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; - - if (domain == null) - { - var defaultLanguage = ApplicationContext.Current.Services.LocalizationService.GetAllLanguages().FirstOrDefault(); - return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); - } - - var wcDomain = DomainHelper.FindWildcardDomainInPath(ApplicationContext.Current.Services.DomainService.GetAll(true), content.Path, domain.RootContent.Id); - return wcDomain == null - ? new CultureInfo(domain.Language.IsoCode) - : new CultureInfo(wcDomain.Language.IsoCode); + return Models.ContentExtensions.GetCulture(UmbracoContext.Current, + ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + content.Id, content.Path, + current); } - + #endregion } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4421f6b251..ab3a8a47ad 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -268,6 +268,7 @@ + From f06a468fb464a3c6dae6181be82747b56b49c4dc Mon Sep 17 00:00:00 2001 From: kgiszewski Date: Fri, 13 Mar 2015 11:40:04 -0400 Subject: [PATCH 049/249] Fix for U4-6395, U4-6117 --- .../lib/tinymce/plugins/umbracolink/plugin.min.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js index c764c51206..e50c347b5a 100644 --- a/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/plugins/umbracolink/plugin.min.js @@ -193,6 +193,7 @@ tinymce.PluginManager.add('umbracolink', function(editor) { }); selection.select(anchorElm); + editor.execCommand('mceEndTyping'); } else { editor.execCommand('mceInsertLink', false, { href: href, From a44b19da187c91908d5f34f87c6c1ff1740ccb44 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 16 Mar 2015 09:16:28 +0100 Subject: [PATCH 050/249] U4-6320 - fix default scrubbing interval, set to 4hrs --- src/Umbraco.Web/Scheduling/Scheduler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index cf2b366e40..ee02947e20 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -82,7 +82,7 @@ namespace Umbraco.Web.Scheduling private int GetLogScrubbingInterval(IUmbracoSettingsSection settings) { - int interval = 24 * 60 * 60; //24 hours + var interval = 4 * 60 * 60 * 1000; // 4 hours, in milliseconds try { if (settings.Logging.CleaningMiliseconds > -1) @@ -90,7 +90,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 4 hours.", e); } return interval; } From 3eb0dac48a37ead2483688bb1a14e56e32b645c2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 16 Mar 2015 09:28:15 +0100 Subject: [PATCH 051/249] U4-3753 - fix the build --- src/Umbraco.Web/Models/ContentExtensions.cs | 15 ++++++--------- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index dc12a90250..4a535bb0e6 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Models public static CultureInfo GetCulture(this IContent content, Uri current = null) { return GetCulture(UmbracoContext.Current, - ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.LocalizationService, content.Id, content.Path, current); } @@ -30,23 +30,20 @@ namespace Umbraco.Web.Models /// within the context of a specified current request. ///
/// An instance. - /// An implementation. /// An implementation. /// The content identifier. /// The content path. /// The request Uri. /// The culture that would be selected to render the content. - public static CultureInfo GetCulture(UmbracoContext umbracoContext, IDomainService domainService, ILocalizationService localizationService, + public static CultureInfo GetCulture(UmbracoContext umbracoContext, ILocalizationService localizationService, int contentId, string contentPath, Uri current) { var route = umbracoContext.ContentCache.GetRouteById(contentId); // cached var pos = route.IndexOf('/'); - var domainHelper = new DomainHelper(domainService); - var domain = pos == 0 ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + : DomainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).Domain; if (domain == null) { @@ -54,10 +51,10 @@ namespace Umbraco.Web.Models return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); } - var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContent.Id); + var wcDomain = DomainHelper.FindWildcardDomainInPath(DomainHelper.GetAllDomains(true), contentPath, domain.RootNodeId); return wcDomain == null - ? new CultureInfo(domain.Language.IsoCode) - : new CultureInfo(wcDomain.Language.IsoCode); + ? new CultureInfo(domain.Language.CultureAlias) + : new CultureInfo(wcDomain.Language.CultureAlias); } } } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 7302913d34..5e750e3f2b 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1898,7 +1898,7 @@ namespace Umbraco.Web public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) { return Models.ContentExtensions.GetCulture(UmbracoContext.Current, - ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.LocalizationService, content.Id, content.Path, current); } From 4974e2a2ce40aeb3e97e5cb428b22867ce8aaeaa Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 16 Mar 2015 10:52:54 +0100 Subject: [PATCH 052/249] U4-6306 - hostname validation issue --- .../umbraco/dialogs/AssignDomain2.aspx | 2 +- .../umbraco_client/Dialogs/AssignDomain2.js | 12 +++++++++--- .../umbraco/dialogs/AssignDomain2.aspx | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx index 2464e9446b..9a69576706 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx @@ -51,7 +51,7 @@ - + diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js index 92cb1b50a9..cfd33541cb 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js @@ -52,9 +52,15 @@ ko.applyBindings(self); $.validator.addMethod("domain", function (value, element, param) { + // beware! encode('test') == 'test-' + // read eg https://rt.cpan.org/Public/Bug/Display.html?id=94347 value = punycode.encode(value); - var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*)?$/gi; - return this.optional(element) || re.test(value); + // that regex is best-effort and certainly not exact + var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; + var isopt = this.optional(element); + var retest = re.test(value); + var ret = isopt || retest; + return ret; }, self._opts.invalidDomain); $.validator.addMethod("duplicate", function (value, element, param) { @@ -85,7 +91,7 @@ return false; var mask = $('#komask'); - var masked = mask.next(); + var masked = mask.parent(); mask.height(masked.height()); mask.width(masked.width()); mask.show(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx index ec6e9f7669..88af0c2007 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx @@ -53,7 +53,7 @@ - + From 2feef247edd564db689d86eb912b67bbf96c3a60 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Mar 2015 19:31:46 +1100 Subject: [PATCH 053/249] Fixes: U4-3562 Cannot preview when no template is assigned --- src/Umbraco.Web.UI.Client/src/views/content/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index b6ad9cb934..e29ee99925 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -44,7 +44,7 @@ -
+
Preview page From d24ef3650e4bc01c0301e00cbe5210c58732d31b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 17 Mar 2015 13:50:33 +0100 Subject: [PATCH 054/249] U4-274 - Adds config option to disable alternative templates --- .../UmbracoSettings/IWebRoutingSection.cs | 2 + .../UmbracoSettings/WebRoutingElement.cs | 6 ++ .../config/umbracoSettings.Release.config | 7 ++- .../config/umbracoSettings.config | 7 ++- .../ContentFinderByNiceUrlAndTemplate.cs | 57 ++++++++++--------- .../Routing/PublishedContentRequestEngine.cs | 5 +- src/Umbraco.Web/Templates/TemplateRenderer.cs | 3 +- .../umbraco.presentation/NotFoundHandlers.cs | 12 ++-- src/Umbraco.Web/umbraco.presentation/page.cs | 38 +++++++------ 9 files changed, 84 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs index 03fad06d82..393387ecfa 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs @@ -6,6 +6,8 @@ bool InternalRedirectPreservesTemplate { get; } + bool DisableAlternativeTemplates { get; } + string UrlProviderMode { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs index c1236f571d..82c5a37575 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs @@ -16,6 +16,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return (bool) base["internalRedirectPreservesTemplate"]; } } + [ConfigurationProperty("disableAlternativeTemplates", DefaultValue = "false")] + public bool DisableAlternativeTemplates + { + get { return (bool) base["disableAlternativeTemplates"]; } + } + [ConfigurationProperty("urlProviderMode", DefaultValue = "AutoLegacy")] public string UrlProviderMode { diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index dcaaa382e8..2959b0388b 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -132,10 +132,15 @@ finder or by the alt. template. Set this option to true to preserve the template set by the finder or by the alt. template, in case of an internal redirect. (false by default, and in fact should remain false unless you know what you're doing) + @disableAlternativeTemplates + By default you can add a altTemplate querystring or append a template name to the current URL which + will make Umbraco render the content on the current page with the template you requested, for example: + http://mysite.com/about-us/?altTemplate=Home and http://mysite.com/about-us/Home would render the + "About Us" page with a template with the alias Home. Setting this setting to true stops that behavior --> + internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false"> diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 08f67f5021..e13441b802 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -290,10 +290,15 @@ finder or by the alt. template. Set this option to true to preserve the template set by the finder or by the alt. template, in case of an internal redirect. (false by default, and in fact should remain false unless you know what you're doing) + @disableAlternativeTemplates + By default you can add a altTemplate querystring or append a template name to the current URL which + will make Umbraco render the content on the current page with the template you requested, for example: + http://mysite.com/about-us/?altTemplate=Home and http://mysite.com/about-us/Home would render the + "About Us" page with a template with the alias Home. Setting this setting to true stops that behavior --> + internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false"> \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs index 21fd81a8e4..246b109dea 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs @@ -1,57 +1,58 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Routing { - /// - /// Provides an implementation of that handles page nice urls and a template. - /// - /// - /// Handles /foo/bar/template where /foo/bar is the nice url of a document, and template a template alias. - /// If successful, then the template of the document request is also assigned. - /// + /// + /// Provides an implementation of that handles page nice urls and a template. + /// + /// + /// Handles /foo/bar/template where /foo/bar is the nice url of a document, and template a template alias. + /// If successful, then the template of the document request is also assigned. + /// public class ContentFinderByNiceUrlAndTemplate : ContentFinderByNiceUrl { - /// - /// Tries to find and assign an Umbraco document to a 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) + /// + /// Tries to find and assign an Umbraco document to a 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) { IPublishedContent node = null; - string path = docRequest.Uri.GetAbsolutePathDecoded(); + string path = docRequest.Uri.GetAbsolutePathDecoded(); - if (docRequest.HasDomain) - path = DomainHelper.PathRelativeToDomain(docRequest.DomainUri, path); + if (docRequest.HasDomain) + path = DomainHelper.PathRelativeToDomain(docRequest.DomainUri, path); - if (path != "/") // no template if "/" + if (path != "/") // no template if "/" { - var pos = path.LastIndexOf('/'); - var templateAlias = path.Substring(pos + 1); - path = pos == 0 ? "/" : path.Substring(0, pos); + var pos = path.LastIndexOf('/'); + var templateAlias = path.Substring(pos + 1); + path = pos == 0 ? "/" : path.Substring(0, pos); var template = ApplicationContext.Current.Services.FileService.GetTemplate(templateAlias); if (template != null) { - LogHelper.Debug("Valid template: \"{0}\"", () => templateAlias); + LogHelper.Debug("Valid template: \"{0}\"", () => templateAlias); - var route = docRequest.HasDomain ? (docRequest.Domain.RootNodeId.ToString() + path) : path; - node = FindContent(docRequest, route); + var route = docRequest.HasDomain ? (docRequest.Domain.RootNodeId.ToString() + path) : path; + node = FindContent(docRequest, route); - if (node != null) - docRequest.TemplateModel = template; + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false && node != null) + docRequest.TemplateModel = template; } else { - LogHelper.Debug("Not a valid template: \"{0}\"", () => templateAlias); + LogHelper.Debug("Not a valid template: \"{0}\"", () => templateAlias); } } else { - LogHelper.Debug("No template in path \"/\""); + LogHelper.Debug("No template in path \"/\""); } return node != null; diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index fee5b44360..0daf1e1c00 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -586,8 +586,9 @@ namespace Umbraco.Web.Routing // only if the published content is the initial once, else the alternate template // does not apply // + optionnally, apply the alternate template on internal redirects - var useAltTemplate = _pcr.IsInitialPublishedContent - || (UmbracoConfig.For.UmbracoSettings().WebRouting.InternalRedirectPreservesTemplate && _pcr.IsInternalRedirectPublishedContent); + var useAltTemplate = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false + && (_pcr.IsInitialPublishedContent + || (UmbracoConfig.For.UmbracoSettings().WebRouting.InternalRedirectPreservesTemplate && _pcr.IsInternalRedirectPublishedContent)); string altTemplate = useAltTemplate ? _routingContext.UmbracoContext.HttpContext.Request[Constants.Conventions.Url.AltTemplate] : null; diff --git a/src/Umbraco.Web/Templates/TemplateRenderer.cs b/src/Umbraco.Web/Templates/TemplateRenderer.cs index a13ed031f8..22d6b5b54b 100644 --- a/src/Umbraco.Web/Templates/TemplateRenderer.cs +++ b/src/Umbraco.Web/Templates/TemplateRenderer.cs @@ -11,6 +11,7 @@ using Umbraco.Web.Mvc; using Umbraco.Web.Routing; using umbraco; using umbraco.cms.businesslogic.language; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Templates { @@ -80,7 +81,7 @@ namespace Umbraco.Web.Templates //set the doc that was found by id contentRequest.PublishedContent = doc; //set the template, either based on the AltTemplate found or the standard template of the doc - contentRequest.TemplateModel = !AltTemplate.HasValue + contentRequest.TemplateModel = UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates || AltTemplate.HasValue == false ? ApplicationContext.Current.Services.FileService.GetTemplate(doc.TemplateId) : ApplicationContext.Current.Services.FileService.GetTemplate(AltTemplate.Value); diff --git a/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs b/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs index ecfa83b1ae..56021687bd 100644 --- a/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs +++ b/src/Umbraco.Web/umbraco.presentation/NotFoundHandlers.cs @@ -13,6 +13,7 @@ using umbraco.cms.businesslogic.web; using umbraco.interfaces; using Umbraco.Core.IO; using umbraco.NodeFactory; +using Umbraco.Core; namespace umbraco { @@ -277,10 +278,13 @@ namespace umbraco { { _redirectID = int.Parse(urlNode.Attributes.GetNamedItem("id").Value); - HttpContext.Current.Items["altTemplate"] = templateAlias; - HttpContext.Current.Trace.Write("umbraco.altTemplateHandler", - string.Format("Templated changed to: '{0}'", - HttpContext.Current.Items["altTemplate"])); + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false) + { + HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate] = templateAlias; + HttpContext.Current.Trace.Write("umbraco.altTemplateHandler", + string.Format("Template changed to: '{0}'", HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate])); + } + _succes = true; } } diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index 6f933ddbd3..ce2b55152b 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -17,6 +17,7 @@ using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.template; using umbraco.cms.businesslogic.web; using umbraco.interfaces; +using Umbraco.Core.Configuration; using Property = umbraco.cms.businesslogic.property.Property; namespace umbraco @@ -152,22 +153,27 @@ namespace umbraco { populatePageData(node); - // Check for alternative template - if (HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate] != null && - HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate].ToString() != String.Empty) - { - _template = - umbraco.cms.businesslogic.template.Template.GetTemplateIdFromAlias( - HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate].ToString()); - _elements.Add("template", _template.ToString()); - } - else if (helper.Request(Constants.Conventions.Url.AltTemplate) != String.Empty) - { - _template = - umbraco.cms.businesslogic.template.Template.GetTemplateIdFromAlias(helper.Request(Constants.Conventions.Url.AltTemplate).ToLower()); - _elements.Add("template", _template.ToString()); - } - if (_template == 0) + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false) + { + // Check for alternative template + if (HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate] != null && + HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate].ToString() != String.Empty) + { + _template = + umbraco.cms.businesslogic.template.Template.GetTemplateIdFromAlias( + HttpContext.Current.Items[Constants.Conventions.Url.AltTemplate].ToString()); + _elements.Add("template", _template.ToString()); + } + else if (helper.Request(Constants.Conventions.Url.AltTemplate) != String.Empty) + { + _template = + umbraco.cms.businesslogic.template.Template.GetTemplateIdFromAlias( + helper.Request(Constants.Conventions.Url.AltTemplate).ToLower()); + _elements.Add("template", _template.ToString()); + } + } + + if (_template == 0) { try { From 7210c6cd95d4ca6dd47c48cf9789e45f46436918 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 17 Mar 2015 17:58:52 +0100 Subject: [PATCH 055/249] U4-6384 Incorrect Error Message When Uploading Large File #U4-6384 Fixed Due in version 7.2.3 --- .../src/common/directives/imaging/umbimagefolder.directive.js | 2 +- .../src/common/services/umbrequesthelper.service.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js index 10f4663588..5b1ad4f0dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/imaging/umbimagefolder.directive.js @@ -108,7 +108,7 @@ function umbImageFolder($rootScope, assetsService, $timeout, $log, umbRequestHel //check for the file size error which can only be done with dodgy string checking scope.$on('fileuploadfail', function (e, data) { if (data.jqXHR.status === 500 && data.jqXHR.responseText.indexOf("Maximum request length exceeded") >= 0) { - notificationsService.error(data.errorThrown, "The image file size was too big, check with your site administrator to adjust the maximum size allowed"); + notificationsService.error(data.errorThrown, "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); } else { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 382c80c3e2..a494a0220a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -225,7 +225,7 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ // we have to just check for the existence of a string value but currently that is the best way to // do this since it's very hacky/difficult to catch this on the server if (data.indexOf("Maximum request length exceeded") >= 0) { - notificationsService.error("Server error", "The image file size was too big, check with your site administrator to adjust the maximum size allowed"); + notificationsService.error("Server error", "The uploaded file was too large, check with your site administrator to adjust the maximum size allowed"); } else if (Umbraco.Sys.ServerVariables["isDebuggingEnabled"] === true) { //show a ysod dialog From 137d8553dea484d2646a157a9ada684ef659a980 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Mar 2015 19:03:46 +1100 Subject: [PATCH 056/249] Fixes: U4-6417 When including a $schema element in package.manifest with a url schema, the regex will incorrectly strip required characters --- src/Umbraco.Core/Manifest/ManifestParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index fbc91de898..6794377001 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Manifest //used to strip comments private static readonly Regex CommentsSurround = new Regex(@"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", RegexOptions.Compiled); - private static readonly Regex CommentsLine = new Regex(@"//.*?$", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex CommentsLine = new Regex(@"^\s*//.*?$", RegexOptions.Compiled | RegexOptions.Multiline); public ManifestParser(DirectoryInfo pluginsDir) { From 9c1d3fabb8eb4c1c56f20f71ba4e3719b215e92c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Mar 2015 19:04:49 +1100 Subject: [PATCH 057/249] Fixes: U4-6418 Loading back office with ClientDependency does not respect the configured value for the compositeFileHandlerPath --- src/Umbraco.Web/WebBootManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index a4adc5af29..8b1c28ed70 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -111,8 +111,12 @@ namespace Umbraco.Web //Register a custom renderer - used to process property editor dependencies var renderer = new DependencyPathRenderer(); - renderer.Initialize("Umbraco.DependencyPathRenderer", new NameValueCollection { { "compositeFileHandlerPath", "~/DependencyHandler.axd" } }); + renderer.Initialize("Umbraco.DependencyPathRenderer", new NameValueCollection + { + { "compositeFileHandlerPath", ClientDependencySettings.Instance.CompositeFileHandlerPath } + }); ClientDependencySettings.Instance.MvcRendererCollection.Add(renderer); + InstallHelper insHelper = new InstallHelper(UmbracoContext.Current); insHelper.DeleteLegacyInstaller(); From 02b0e4dd6a5a0aabd2e2de14ce565624aa608705 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Mar 2015 19:06:41 +1100 Subject: [PATCH 058/249] Fixes: U4-6380 ClientDependency Bug on server - Can't Access Backoffice --- .../UI/JavaScript/AssetInitialization.cs | 63 +++++++++---------- .../UI/JavaScript/CssInitialization.cs | 2 +- .../UI/JavaScript/JsInitialization.cs | 5 +- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Web/UI/JavaScript/AssetInitialization.cs b/src/Umbraco.Web/UI/JavaScript/AssetInitialization.cs index 651f6bde07..26f933f734 100644 --- a/src/Umbraco.Web/UI/JavaScript/AssetInitialization.cs +++ b/src/Umbraco.Web/UI/JavaScript/AssetInitialization.cs @@ -47,51 +47,47 @@ namespace Umbraco.Web.UI.JavaScript } /// - /// This will check if we're in release mode, if so it will create a CDF URL to load them all in at once + /// This will use CDF to optimize the asset file collection /// /// /// /// - /// - protected JArray CheckIfReleaseAndOptimized(JArray fileRefs, ClientDependencyType cdfType, HttpContextBase httpContext) + /// + /// Return the asset URLs that should be loaded, if the application is in debug mode then the URLs returned will be the same as the ones + /// passed in with the CDF version query strings appended so cache busting works correctly. + /// + protected JArray OptimizeAssetCollection(JArray fileRefs, ClientDependencyType cdfType, HttpContextBase httpContext) { if (httpContext == null) throw new ArgumentNullException("httpContext"); - - if (httpContext.IsDebuggingEnabled == false) - { - return GetOptimized(fileRefs, cdfType, httpContext); - } - return fileRefs; - } - /// - /// Return array of optimized URLs - /// - /// - /// - /// - /// - protected JArray GetOptimized(JArray fileRefs, ClientDependencyType cdfType, HttpContextBase httpContext) - { var depenencies = fileRefs.Select(x => + { + var asString = x.ToString(); + if (asString.StartsWith("/") == false) { - var asString = x.ToString(); - if (asString.StartsWith("/") == false) + //most declarations with be made relative to the /umbraco folder, so things like lib/blah/blah.js + // so we need to turn them into absolutes here + if (Uri.IsWellFormedUriString(asString, UriKind.Relative)) { - if (Uri.IsWellFormedUriString(asString, UriKind.Relative)) - { - var absolute = new Uri(httpContext.Request.Url, asString); - return new BasicFile(cdfType) { FilePath = absolute.AbsolutePath }; - } - return null; + var absolute = new Uri(httpContext.Request.Url, asString); + return (IClientDependencyFile)new BasicFile(cdfType) { FilePath = absolute.AbsolutePath }; } - return new JavascriptFile(asString); - }).Where(x => x != null); + } + return cdfType == ClientDependencyType.Javascript + ? (IClientDependencyFile)new JavascriptFile(asString) + : (IClientDependencyFile)new CssFile(asString); + }).Where(x => x != null).ToList(); - var urls = ClientDependencySettings.Instance.DefaultCompositeFileProcessingProvider.ProcessCompositeList( - depenencies, - cdfType, - httpContext); + //Get the output string for these registrations which will be processed by CDF correctly to stagger the output based + // on internal vs external resources. The output will be delimited based on our custom Umbraco.Web.UI.JavaScript.DependencyPathRenderer + string jsOut; + string cssOut; + var renderer = ClientDependencySettings.Instance.MvcRendererCollection["Umbraco.DependencyPathRenderer"]; + renderer.RegisterDependencies(depenencies, new HashSet(), out jsOut, out cssOut, httpContext); + + var urls = cdfType == ClientDependencyType.Javascript + ? jsOut.Split(new string[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries) + : cssOut.Split(new string[] { DependencyPathRenderer.Delimiter }, StringSplitOptions.RemoveEmptyEntries); var result = new JArray(); foreach (var u in urls) @@ -100,5 +96,6 @@ namespace Umbraco.Web.UI.JavaScript } return result; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/UI/JavaScript/CssInitialization.cs b/src/Umbraco.Web/UI/JavaScript/CssInitialization.cs index c7772944c2..19f932e83d 100644 --- a/src/Umbraco.Web/UI/JavaScript/CssInitialization.cs +++ b/src/Umbraco.Web/UI/JavaScript/CssInitialization.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web.UI.JavaScript } //now we can optimize if in release mode - merged = CheckIfReleaseAndOptimized(merged, ClientDependencyType.Css, httpContext); + merged = OptimizeAssetCollection(merged, ClientDependencyType.Css, httpContext); //now we need to merge in any found cdf declarations on property editors ManifestParser.MergeJArrays(merged, ScanPropertyEditors(ClientDependencyType.Css, httpContext)); diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialization.cs b/src/Umbraco.Web/UI/JavaScript/JsInitialization.cs index ff52a6b65d..34d297a86f 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialization.cs +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialization.cs @@ -45,9 +45,6 @@ namespace Umbraco.Web.UI.JavaScript public string GetJavascriptInitialization(HttpContextBase httpContext, JArray umbracoInit, JArray additionalJsFiles = null) { var result = GetJavascriptInitializationArray(httpContext, umbracoInit, additionalJsFiles); - //create a unique hash code of the current umb version and the current cdf version - var versionHash = UrlHelperExtensions.GetCacheBustHash(); - var version = "'" + versionHash + "'"; return ParseMain( result.ToString(), @@ -68,7 +65,7 @@ namespace Umbraco.Web.UI.JavaScript } //now we can optimize if in release mode - umbracoInit = CheckIfReleaseAndOptimized(umbracoInit, ClientDependencyType.Javascript, httpContext); + umbracoInit = OptimizeAssetCollection(umbracoInit, ClientDependencyType.Javascript, httpContext); //now we need to merge in any found cdf declarations on property editors ManifestParser.MergeJArrays(umbracoInit, ScanPropertyEditors(ClientDependencyType.Javascript, httpContext)); From 9aa5df43bf4ed90e2ee02dc3bad6803c17676cd3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Mar 2015 19:14:47 +1100 Subject: [PATCH 059/249] Fixes: U4-5956 Feature request: Enhance the preview mode with labels clearly explaining the current view --- .../src/canvasdesigner/canvasdesigner.controller.js | 12 ++++++------ .../src/canvasdesigner/index.html | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js index 403a05a5e1..127d5882dc 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js @@ -15,12 +15,12 @@ var app = angular.module("Umbraco.canvasdesigner", ['colorpicker', 'ui.slider', $scope.pageUrl = "../dialogs/Preview.aspx?id=" + $location.search().id; $scope.valueAreLoaded = false; $scope.devices = [ - { name: "desktop", css: "desktop", icon: "icon-display" }, - { name: "laptop - 1366px", css: "laptop border", icon: "icon-laptop" }, - { name: "iPad portrait - 768px", css: "iPad-portrait border", icon: "icon-ipad" }, - { name: "iPad landscape - 1024px", css: "iPad-landscape border", icon: "icon-ipad flip" }, - { name: "smartphone portrait - 480px", css: "smartphone-portrait border", icon: "icon-iphone" }, - { name: "smartphone landscape - 320px", css: "smartphone-landscape border", icon: "icon-iphone flip" } + { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, + { name: "laptop - 1366px", css: "laptop border", icon: "icon-laptop", title: "Laptop" }, + { name: "iPad portrait - 768px", css: "iPad-portrait border", icon: "icon-ipad", title: "Tablet portrait" }, + { name: "iPad landscape - 1024px", css: "iPad-landscape border", icon: "icon-ipad flip", title: "Tablet landscape" }, + { name: "smartphone portrait - 480px", css: "smartphone-portrait border", icon: "icon-iphone", title: "Smartphone portrait" }, + { name: "smartphone landscape - 320px", css: "smartphone-landscape border", icon: "icon-iphone flip", title: "Smartphone landscape" } ]; $scope.previewDevice = $scope.devices[0]; diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/index.html b/src/Umbraco.Web.UI.Client/src/canvasdesigner/index.html index 0e150965d6..3db48d1940 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/index.html +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner/index.html @@ -25,7 +25,7 @@

asdfasdf

".Replace(Environment.NewLine, string.Empty), result.Replace(Environment.NewLine, string.Empty)); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js index bcbe2ca63a..e60ee01ff0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js @@ -13,7 +13,9 @@ function macroService() { /** parses the special macro syntax like and returns an object with the macro alias and it's parameters */ parseMacroSyntax: function (syntax) { - var expression = /(<\?UMBRACO_MACRO macroAlias=["']([\s\S.]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; + //This regex will match an alias of anything except characters that are quotes or new lines (for legacy reasons, when new macros are created + // their aliases are cleaned an invalid chars are stripped) + var expression = /(<\?UMBRACO_MACRO macroAlias=["']([^\"\'\n\r]+?)["'][\s\S]+?)(\/>|>.*?<\/\?UMBRACO_MACRO>)/i; var match = expression.exec(syntax); if (!match || match.length < 3) { return null; diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/macro-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/macro-service.spec.js index 535d134b4b..bdb9cfbd17 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/macro-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/macro-service.spec.js @@ -39,6 +39,18 @@ describe('macro service tests', function () { expect(result.macroParamsDictionary.test2).not.toBeUndefined(); expect(result.macroParamsDictionary.test2).toBe("hello"); }); + + it('can parse syntax for macros with aliases containing whitespace and other chars', function () { + + var result = macroService.parseMacroSyntax(""); + + expect(result).not.toBeNull(); + expect(result.macroAlias).toBe("Map Test [Hello\\World]"); + expect(result.macroParamsDictionary.test).not.toBeUndefined(); + expect(result.macroParamsDictionary.test).toBe("asdf"); + expect(result.macroParamsDictionary.test2).not.toBeUndefined(); + expect(result.macroParamsDictionary.test2).toBe("hello"); + }); it('can parse syntax for macros with body', function () { From f742c0a95869f30dca578c3a0e4cbc534bb52271 Mon Sep 17 00:00:00 2001 From: Helmuth Bederna Date: Wed, 18 Mar 2015 10:41:15 +0100 Subject: [PATCH 062/249] fixed issue U4-6419 by removing that character --- src/Umbraco.Web/Models/TagModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Models/TagModel.cs b/src/Umbraco.Web/Models/TagModel.cs index d0845ff8f8..ef6b0a86c1 100644 --- a/src/Umbraco.Web/Models/TagModel.cs +++ b/src/Umbraco.Web/Models/TagModel.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Models [DataMember(Name = "group")] public string Group { get; set; } - [DataMember(Name = "nodeCount`")] + [DataMember(Name = "nodeCount")] public int NodeCount { get; set; } } } From c5f26c7c158ed7f7f23e29990ab6c8e3c07876c7 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Wed, 18 Mar 2015 11:44:49 +0100 Subject: [PATCH 063/249] Fixes: U4-6331 The grid tools overlay disable the links/buttons on smaller screens --- src/Umbraco.Web.UI.Client/src/less/gridview.less | 2 ++ .../src/views/propertyeditors/grid/grid.html | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 778acc5607..107e042a16 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -155,6 +155,8 @@ IFRAME {overflow:hidden;} opacity: 0.3; z-index: 50; } +//special rule to ensure forms doesnt overrride (forms will align in a later release) +.umb-grid .cell-tools{width: 50px !important;} .usky-grid .cell-tools.with-prompt { width:200px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 6f092c8ea8..cd3e504156 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -1,4 +1,4 @@ -
+
@@ -57,7 +57,8 @@ ng-mouseleave="disableCurrentRow()"> -
+
From c2cfd26ee6f86d16b25c7ca1f8511d327dc759a1 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Wed, 18 Mar 2015 11:45:04 +0100 Subject: [PATCH 064/249] Fixes broken bower jquery reference --- src/Umbraco.Web.UI.Client/bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..23744b0ff7 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -38,7 +38,7 @@ "": "tmhDynamicLocale.min.{js,js.map}" }, "jquery": { - "": "jquery.min.{js,map}" + "": "dist/jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" From 3a9ad91f8ff8d60f29d7579ca04c0acf403caf7a Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Wed, 18 Mar 2015 11:45:22 +0100 Subject: [PATCH 065/249] Adds better error handling to missing grid editors --- .../propertyeditors/grid/editors/error.html | 2 ++ .../propertyeditors/grid/grid.controller.js | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/error.html diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/error.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/error.html new file mode 100644 index 0000000000..67ef4ea112 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/error.html @@ -0,0 +1,2 @@ +

Something went wrong with this editor, below is the data stored:

+
{{control | json}}
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index f802173a25..11afb678a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -572,18 +572,31 @@ angular.module("umbraco") control.$index = index; control.$uniqueId = $scope.setUniqueId(); + //error handling in case of missing editor.. + //should only happen if stripped earlier + if(!control.editor){ + control.$editorPath = "views/propertyeditors/grid/editors/error.html"; + } + if(!control.$editorPath){ var editorConfig = $scope.getEditor(control.editor.alias); - control.editor = editorConfig; - //if its a path - if(_.indexOf(control.editor.view, "/") >= 0){ - control.$editorPath = control.editor.view; + if(editorConfig){ + control.editor = editorConfig; + + //if its a path + if(_.indexOf(control.editor.view, "/") >= 0){ + control.$editorPath = control.editor.view; + }else{ + //use convention + control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; + } }else{ - //use convention - control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; + control.$editorPath = "views/propertyeditors/grid/editors/error.html"; } } + + }; From 00fbc6dfcb5a48bb6f4c7b0bb3d2ae25f54361bc Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 18 Mar 2015 13:05:21 +0100 Subject: [PATCH 066/249] Revert "Fixes broken bower jquery reference" This reverts commit c2cfd26ee6f86d16b25c7ca1f8511d327dc759a1. --- src/Umbraco.Web.UI.Client/bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 23744b0ff7..e8ad94653f 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -38,7 +38,7 @@ "": "tmhDynamicLocale.min.{js,js.map}" }, "jquery": { - "": "dist/jquery.min.{js,map}" + "": "jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" From 6cf25f0043c182b982aed7fddc6045592402f8de Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 11:35:04 +1100 Subject: [PATCH 067/249] Fixes: U4-6428 Grid configuration does not support virtual paths - and therefore is not compatible with virtual directories --- .../services/umbrequesthelper.service.js | 24 +++++++++++++++++++ .../propertyeditors/grid/grid.controller.js | 11 +++++---- .../umbraco/Views/Default.cshtml | 3 +++ .../Editors/BackOfficeController.cs | 10 +++++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index a494a0220a..963e2af06e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -6,6 +6,30 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogService, notificationsService) { return { + /** + * @ngdoc method + * @name umbraco.services.umbRequestHelper#convertVirtualToAbsolutePath + * @methodOf umbraco.services.umbRequestHelper + * @function + * + * @description + * This will convert a virtual path (i.e. ~/App_Plugins/Blah/Test.html ) to an absolute path + * + * @param {string} a virtual path, if this is already an absolute path it will just be returned, if this is a relative path an exception will be thrown + */ + convertVirtualToAbsolutePath: function(virtualPath) { + if (virtualPath.startsWith("/")) { + return virtualPath; + } + if (!virtualPath.startsWith("~/")) { + throw "The path " + virtualPath + " is not a virtual path"; + } + if (!Umbraco.Sys.ServerVariables.application.applicationPath) { + throw "No applicationPath defined in Umbraco.ServerVariables.application.applicationPath"; + } + return Umbraco.Sys.ServerVariables.application.applicationPath + virtualPath.trimStart("~/"); + }, + /** * @ngdoc method * @name umbraco.services.umbRequestHelper#dictionaryToQueryString diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index f802173a25..8851f74b34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridController", - function ($scope, $http, assetsService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout) { + function ($scope, $http, assetsService, $rootScope, dialogService, gridService, mediaResource, imageHelper, $timeout, umbRequestHelper) { // Grid status variables $scope.currentRow = null; @@ -576,10 +576,11 @@ angular.module("umbraco") var editorConfig = $scope.getEditor(control.editor.alias); control.editor = editorConfig; - //if its a path - if(_.indexOf(control.editor.view, "/") >= 0){ - control.$editorPath = control.editor.view; - }else{ + //if its an absolute path + if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { + control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); + } + else { //use convention control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; } diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index bef809a593..308f483af2 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -58,6 +58,9 @@ "umbracoUrls": { "authenticationApiBaseUrl": "@(Url.GetUmbracoApiServiceBaseUrl(controller => controller.PostLogin(null)))", "serverVarsJs": '@Url.GetUrlWithCacheBust("ServerVariables", "BackOffice")' + }, + "application": { + "applicationPath" : "@Context.Request.ApplicationPath" } }; diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index b353b3ce27..003104de63 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -281,15 +281,17 @@ namespace Umbraco.Web.Editors } }, {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, - { - "application", GetApplicationState() - } + {"application", GetApplicationState()} }; return JavaScript(ServerVariablesParser.Parse(d)); } + /// + /// Returns the server variables regarding the application state + /// + /// private Dictionary GetApplicationState() { if (ApplicationContext.IsConfigured == false) @@ -306,6 +308,8 @@ namespace Umbraco.Web.Editors app.Add("version", version); app.Add("cdf", ClientDependency.Core.Config.ClientDependencySettings.Instance.Version); + //useful for dealing with virtual paths on the client side when hosted in virtual directories especially + app.Add("applicationPath", HttpContext.Request.ApplicationPath.EnsureEndsWith('/')); return app; } From 1a082464e98bfd4a8515ecd44b87aa0711622287 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 11:44:37 +1100 Subject: [PATCH 068/249] Fixes: U4-5127 AssetService does not support virtual paths (~/) - and fixes up some formatting --- .../src/common/services/assets.service.js | 150 ++++++++++-------- .../propertyeditors/grid/grid.controller.js | 139 ++++++++-------- 2 files changed, 153 insertions(+), 136 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js index 057e9c3b27..28ac072ea6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js @@ -44,27 +44,36 @@ angular.module('umbraco.services') .factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) { var initAssetsLoaded = false; - var appendRnd = function(url){ + var appendRnd = function (url) { //if we don't have a global umbraco obj yet, the app is bootstrapping - if(!Umbraco.Sys.ServerVariables.application){ + if (!Umbraco.Sys.ServerVariables.application) { return url; } - var rnd = Umbraco.Sys.ServerVariables.application.version +"."+Umbraco.Sys.ServerVariables.application.cdf; - var _op = (url.indexOf("?")>0) ? "&" : "?"; + var rnd = Umbraco.Sys.ServerVariables.application.version + "." + Umbraco.Sys.ServerVariables.application.cdf; + var _op = (url.indexOf("?") > 0) ? "&" : "?"; url = url + _op + "umb__rnd=" + rnd; return url; }; + function convertVirtualPath(path) { + //make this work for virtual paths + if (path.startsWith("~/")) { + path = umbRequestHelper.convertVirtualToAbsolutePath(path); + } + return path; + } + var service = { - loadedAssets:{}, - - _getAssetPromise : function(path){ - if(this.loadedAssets[path]){ + loadedAssets: {}, + + _getAssetPromise: function (path) { + + if (this.loadedAssets[path]) { return this.loadedAssets[path]; - }else{ + } else { var deferred = $q.defer(); - this.loadedAssets[path] = {deferred: deferred, state: "new", path: path}; + this.loadedAssets[path] = { deferred: deferred, state: "new", path: path }; return this.loadedAssets[path]; } }, @@ -77,7 +86,7 @@ angular.module('umbraco.services') //here we need to ensure the required application assets are loaded if (initAssetsLoaded === false) { var self = this; - self.loadJs(umbRequestHelper.getApiUrl("serverVarsJs", "", ""), $rootScope).then(function() { + self.loadJs(umbRequestHelper.getApiUrl("serverVarsJs", "", ""), $rootScope).then(function () { initAssetsLoaded = true; //now we need to go get the legacyTreeJs - but this can be done async without waiting. @@ -106,30 +115,33 @@ angular.module('umbraco.services') * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ - loadCss : function(path, scope, attributes, timeout){ - var asset = this._getAssetPromise(path); // $q.defer(); - var t = timeout || 5000; - var a = attributes || undefined; - - if(asset.state === "new"){ - asset.state = "loading"; - LazyLoad.css(appendRnd(path), function () { - if (!scope) { - asset.state = "loaded"; - asset.deferred.resolve(true); - }else{ - asset.state = "loaded"; - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - }else if(asset.state === "loaded"){ - asset.deferred.resolve(true); - } - return asset.deferred.promise; - }, - + loadCss: function (path, scope, attributes, timeout) { + + path = convertVirtualPath(path); + + var asset = this._getAssetPromise(path); // $q.defer(); + var t = timeout || 5000; + var a = attributes || undefined; + + if (asset.state === "new") { + asset.state = "loading"; + LazyLoad.css(appendRnd(path), function () { + if (!scope) { + asset.state = "loaded"; + asset.deferred.resolve(true); + } else { + asset.state = "loaded"; + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } + }); + } else if (asset.state === "loaded") { + asset.deferred.resolve(true); + } + return asset.deferred.promise; + }, + /** * @ngdoc method * @name umbraco.services.assetsService#loadJs @@ -144,28 +156,30 @@ angular.module('umbraco.services') * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ - loadJs : function(path, scope, attributes, timeout){ - + loadJs: function (path, scope, attributes, timeout) { + + path = convertVirtualPath(path); + var asset = this._getAssetPromise(path); // $q.defer(); var t = timeout || 5000; var a = attributes || undefined; - - if(asset.state === "new"){ + + if (asset.state === "new") { asset.state = "loading"; LazyLoad.js(appendRnd(path), function () { - if (!scope) { - asset.state = "loaded"; - asset.deferred.resolve(true); - }else{ - asset.state = "loaded"; - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } + if (!scope) { + asset.state = "loaded"; + asset.deferred.resolve(true); + } else { + asset.state = "loaded"; + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } }); - }else if(asset.state === "loaded"){ + } else if (asset.state === "loaded") { asset.deferred.resolve(true); } @@ -192,7 +206,7 @@ angular.module('umbraco.services') throw "pathArray must be an array"; } - var nonEmpty = _.reject(pathArray, function(item) { + var nonEmpty = _.reject(pathArray, function (item) { return item === undefined || item === ""; }); @@ -204,12 +218,14 @@ angular.module('umbraco.services') //compile a list of promises //blocking - _.each(nonEmpty, function(path){ + _.each(nonEmpty, function (path) { + + path = convertVirtualPath(path); + var asset = service._getAssetPromise(path); //if not previously loaded, add to list of promises - if(asset.state !== "loaded") - { - if(asset.state === "new"){ + if (asset.state !== "loaded") { + if (asset.state === "new") { asset.state = "loading"; assets.push(asset); } @@ -223,21 +239,21 @@ angular.module('umbraco.services') //gives a central monitoring of all assets to load promise = $q.all(promises); - - _.each(assets, function(asset){ + + _.each(assets, function (asset) { LazyLoad.js(appendRnd(asset.path), function () { - if (!scope) { - asset.state = "loaded"; - asset.deferred.resolve(true); - }else{ - asset.state = "loaded"; - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); + if (!scope) { + asset.state = "loaded"; + asset.deferred.resolve(true); + } else { + asset.state = "loaded"; + angularHelper.safeApply(scope, function () { + asset.deferred.resolve(true); + }); + } + }); }); - }else{ + } else { //return and resolve var deferred = $q.defer(); promise = deferred.promise; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index f4a61a597e..883ba5483b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -63,7 +63,7 @@ angular.module("umbraco") handle: '.cell-tools-move', connectWith: ".usky-cell", forcePlaceholderSize: true, - tolerance:"pointer", + tolerance: "pointer", zIndex: 999999999999999999, scrollSensitivity: 100, cursorAt: { @@ -152,7 +152,7 @@ angular.module("umbraco") key: undefined }; - $scope.addItemOverlay = function(event, area, index, key){ + $scope.addItemOverlay = function (event, area, index, key) { $scope.overlayMenu.area = area; $scope.overlayMenu.index = index; $scope.overlayMenu.style = {}; @@ -163,10 +163,10 @@ angular.module("umbraco") var height = $(window).height(); var width = $(window).width(); - if((height-offset.top) < 250){ + if ((height - offset.top) < 250) { $scope.overlayMenu.style.bottom = 0; $scope.overlayMenu.style.top = "initial"; - }else if(offset.top < 300){ + } else if (offset.top < 300) { $scope.overlayMenu.style.top = 190; } @@ -187,7 +187,7 @@ angular.module("umbraco") $scope.model.value = angular.copy(template); //default row data - _.forEach($scope.model.value.sections, function(section){ + _.forEach($scope.model.value.sections, function (section) { $scope.initSection(section); }); }; @@ -221,14 +221,14 @@ angular.module("umbraco") $scope.currentInfohighlightRow = null; }; - $scope.getAllowedLayouts = function(column){ + $scope.getAllowedLayouts = function (column) { var layouts = $scope.model.config.items.layouts; - if(column.allowed && column.allowed.length > 0){ - return _.filter(layouts, function(layout){ + if (column.allowed && column.allowed.length > 0) { + return _.filter(layouts, function (layout) { return _.indexOf(column.allowed, layout.name) >= 0; }); - }else{ + } else { return layouts; } }; @@ -242,8 +242,8 @@ angular.module("umbraco") row = $scope.initRow(row); // Push the new row - if(row){ - section.rows.push(row); + if (row) { + section.rows.push(row); } }; @@ -264,7 +264,7 @@ angular.module("umbraco") gridItem: gridItem, config: $scope.model.config, itemType: itemType, - callback: function(data){ + callback: function (data) { gridItem.styles = data.styles; gridItem.config = data.config; @@ -286,11 +286,11 @@ angular.module("umbraco") $scope.currentCell = null; }; - $scope.cellPreview = function(cell){ - if(cell && cell.$allowedEditors){ + $scope.cellPreview = function (cell) { + if (cell && cell.$allowedEditors) { var editor = cell.$allowedEditors[0]; return editor.icon; - }else{ + } else { return "icon-layout"; } }; @@ -355,7 +355,7 @@ angular.module("umbraco") }; })(); - $scope.addControl = function (editor, cell, index){ + $scope.addControl = function (editor, cell, index) { $scope.closeItemOverlay(); var newControl = { @@ -369,18 +369,18 @@ angular.module("umbraco") } //populate control - $scope.initControl(newControl, index+1); + $scope.initControl(newControl, index + 1); cell.controls.splice(index + 1, 0, newControl); }; - $scope.addTinyMce = function(cell){ + $scope.addTinyMce = function (cell) { var rte = $scope.getEditor("rte"); $scope.addControl(rte, cell); }; - $scope.getEditor = function(alias){ - return _.find($scope.availableEditors, function(editor){return editor.alias === alias;}); + $scope.getEditor = function (alias) { + return _.find($scope.availableEditors, function (editor) { return editor.alias === alias; }); }; $scope.removeControl = function (cell, $index) { @@ -388,12 +388,12 @@ angular.module("umbraco") cell.controls.splice($index, 1); }; - $scope.percentage = function(spans){ - return (( spans/ $scope.model.config.items.columns ) *100).toFixed(1); + $scope.percentage = function (spans) { + return ((spans / $scope.model.config.items.columns) * 100).toFixed(1); }; - $scope.clearPrompt = function(scopedObject, e) { + $scope.clearPrompt = function (scopedObject, e) { scopedObject.deletePrompt = false; e.preventDefault(); e.stopPropagation(); @@ -416,68 +416,68 @@ angular.module("umbraco") // ********************************************* // Init template + sections // ********************************************* - $scope.initContent = function() { + $scope.initContent = function () { var clear = true; //settings indicator shortcut - if($scope.model.config.items.config || $scope.model.config.items.styles){ + if ($scope.model.config.items.config || $scope.model.config.items.styles) { $scope.hasSettings = true; } //ensure the grid has a column value set, if nothing is found, set it to 12 - if($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)){ + if ($scope.model.config.items.columns && angular.isString($scope.model.config.items.columns)) { $scope.model.config.items.columns = parseInt($scope.model.config.items.columns); - }else{ + } else { $scope.model.config.items.columns = 12; } if ($scope.model.value && $scope.model.value.sections && $scope.model.value.sections.length > 0) { - _.forEach($scope.model.value.sections, function(section, index){ + _.forEach($scope.model.value.sections, function (section, index) { - if(section.grid > 0){ + if (section.grid > 0) { $scope.initSection(section); //we do this to ensure that the grid can be reset by deleting the last row - if(section.rows.length > 0){ + if (section.rows.length > 0) { clear = false; } - }else{ + } else { $scope.model.value.sections.splice(index, 1); } }); - }else if($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1){ + } else if ($scope.model.config.items.templates && $scope.model.config.items.templates.length === 1) { $scope.addTemplate($scope.model.config.items.templates[0]); } - if(clear){ + if (clear) { $scope.model.value = undefined; } }; - $scope.initSection = function(section){ + $scope.initSection = function (section) { section.$percentage = $scope.percentage(section.grid); var layouts = $scope.model.config.items.layouts; - if(section.allowed && section.allowed.length > 0){ - section.$allowedLayouts = _.filter(layouts, function(layout){ + if (section.allowed && section.allowed.length > 0) { + section.$allowedLayouts = _.filter(layouts, function (layout) { return _.indexOf(section.allowed, layout.name) >= 0; }); - }else{ + } else { section.$allowedLayouts = layouts; } - if(!section.rows){ + if (!section.rows) { section.rows = []; - }else{ - _.forEach(section.rows, function(row, index){ - if(!row.$initialized){ + } else { + _.forEach(section.rows, function (row, index) { + if (!row.$initialized) { var initd = $scope.initRow(row); //if init fails, remove - if(!initd){ + if (!initd) { section.rows.splice(index, 1); - }else{ + } else { section.rows[index] = initd; } } @@ -489,25 +489,25 @@ angular.module("umbraco") // ********************************************* // Init layout / row // ********************************************* - $scope.initRow = function(row){ + $scope.initRow = function (row) { //merge the layout data with the original config data //if there are no config info on this, splice it out var original = _.find($scope.model.config.items.layouts, function (o) { return o.name === row.name; }); - if(!original){ + if (!original) { return null; - }else{ + } else { //make a copy to not touch the original config original = angular.copy(original); original.styles = row.styles; original.config = row.config; //sync area configuration - _.each(original.areas, function(area, areaIndex){ + _.each(original.areas, function (area, areaIndex) { - if(area.grid > 0){ + if (area.grid > 0) { var currentArea = row.areas[areaIndex]; if (currentArea) { @@ -516,14 +516,14 @@ angular.module("umbraco") } //copy over existing controls into the new areas - if(row.areas.length > areaIndex && row.areas[areaIndex].controls){ + if (row.areas.length > areaIndex && row.areas[areaIndex].controls) { area.controls = currentArea.controls; - _.forEach(area.controls, function(control, controlIndex){ + _.forEach(area.controls, function (control, controlIndex) { $scope.initControl(control, controlIndex); }); - }else{ + } else { area.controls = []; } @@ -532,19 +532,19 @@ angular.module("umbraco") area.$uniqueId = $scope.setUniqueId(); //set editor permissions - if(!area.allowed || area.allowAll === true){ + if (!area.allowed || area.allowAll === true) { area.$allowedEditors = $scope.availableEditors; area.$allowsRTE = true; - }else{ - area.$allowedEditors = _.filter($scope.availableEditors, function(editor){ + } else { + area.$allowedEditors = _.filter($scope.availableEditors, function (editor) { return _.indexOf(area.allowed, editor.alias) >= 0; }); - if(_.indexOf(area.allowed,"rte")>=0){ + if (_.indexOf(area.allowed, "rte") >= 0) { area.$allowsRTE = true; } } - }else{ + } else { original.areas.splice(areaIndex, 1); } }); @@ -568,40 +568,41 @@ angular.module("umbraco") // Init control // ********************************************* - $scope.initControl = function(control, index){ + $scope.initControl = function (control, index) { control.$index = index; control.$uniqueId = $scope.setUniqueId(); //error handling in case of missing editor.. //should only happen if stripped earlier - if(!control.editor){ + if (!control.editor) { control.$editorPath = "views/propertyeditors/grid/editors/error.html"; } - if(!control.$editorPath){ + if (!control.$editorPath) { var editorConfig = $scope.getEditor(control.editor.alias); - if(editorConfig){ + if (editorConfig) { control.editor = editorConfig; - - //if its an absolute path - if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { - control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); - } - else { + + //if its an absolute path + if (control.editor.view.startsWith("/") || control.editor.view.startsWith("~/")) { + control.$editorPath = umbRequestHelper.convertVirtualToAbsolutePath(control.editor.view); + } + else { //use convention control.$editorPath = "views/propertyeditors/grid/editors/" + control.editor.view + ".html"; } - }else{ + } + else { control.$editorPath = "views/propertyeditors/grid/editors/error.html"; } } - + }; - gridService.getGridEditors().then(function(response){ + gridService.getGridEditors().then(function (response) { $scope.availableEditors = response.data; $scope.contentReady = true; From 5737cdfbe271efb55a11f428ea886206d57bb25e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 12:03:55 +1100 Subject: [PATCH 069/249] Fixes the way HttpResponseMessages are created - they should always be created with Request.CreateReponse! --- .../Editors/CanvasDesignerController.cs | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web/Editors/CanvasDesignerController.cs b/src/Umbraco.Web/Editors/CanvasDesignerController.cs index 17dfbe7ab5..722167d18a 100644 --- a/src/Umbraco.Web/Editors/CanvasDesignerController.cs +++ b/src/Umbraco.Web/Editors/CanvasDesignerController.cs @@ -40,10 +40,8 @@ namespace Umbraco.Web.Editors response = client.DownloadString(new Uri(googleWebFontAPIURL)); } - var resp = new HttpResponseMessage() - { - Content = new StringContent(response) - }; + var resp = Request.CreateResponse(); + resp.Content = new StringContent(response); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return resp; @@ -69,10 +67,8 @@ namespace Umbraco.Web.Editors } // Response - var resp = new HttpResponseMessage() - { - Content = new StringContent("{" + String.Join(",", parameters) + "}") - }; + var resp = Request.CreateResponse(); + resp.Content = new StringContent("{" + String.Join(",", parameters) + "}"); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return resp; } @@ -89,10 +85,8 @@ namespace Umbraco.Web.Editors // Save and compile styles CanvasDesignerUtility.SaveAndPublishStyle(parameters, pageId, inherited); - var resp = new HttpResponseMessage() - { - Content = new StringContent("ok") - }; + var resp = Request.CreateResponse(); + resp.Content = new StringContent("ok"); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return resp; @@ -106,10 +100,8 @@ namespace Umbraco.Web.Editors CanvasDesignerUtility.DeleteStyle(int.Parse(pageId)); - var resp = new HttpResponseMessage() - { - Content = new StringContent("ok") - }; + var resp = Request.CreateResponse(); + resp.Content = new StringContent("ok"); resp.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return resp; From 9469b0b844d47328a557cf66e517c3059f646ebd Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 13:53:15 +1100 Subject: [PATCH 070/249] Fixes: U4-6402 Grid config file should be merged with package.manifests & U4-6427 Grid config file has caching problems because it's downloaded as a static file This also fixes up the OutputCaching params on the BackOfficeController since OutputCache is bypassed when an action requires authentication, so now we manually do some caching when not in debug mode for authorized actions (of course auth happens before any cached response can occur). This also fixes up the static caching that was happening with the ManifestBuilder so now when that is not in use it gives back it's memory. This also fixes up any client side caching that was happening on BackOfficeController - before we were allowing client cache to happen for a few actions on that controller which is incorrect, we need to disable all client cache for all actions on that controller. --- src/Umbraco.Core/CoreBootManager.cs | 11 +- src/Umbraco.Core/Manifest/ManifestBuilder.cs | 97 ++-- src/Umbraco.Core/Manifest/ManifestParser.cs | 47 +- src/Umbraco.Core/Manifest/PackageManifest.cs | 5 + .../PropertyEditors/GridEditor.cs | 62 +++ .../ParameterEditorResolver.cs | 11 +- .../PropertyEditors/PropertyEditorResolver.cs | 14 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Manifest/ManifestParserTests.cs | 55 +++ .../src/common/services/grid.service.js | 2 +- .../Editors/BackOfficeController.cs | 427 +++++++++++------- .../Mvc/DisableClientCacheAttribute.cs | 25 + .../Mvc/MinifyJavaScriptResultAttribute.cs | 5 +- 13 files changed, 533 insertions(+), 229 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/GridEditor.cs create mode 100644 src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index f59b6e37ab..16ffc828a9 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Web; using AutoMapper; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.PublishedContent; @@ -21,6 +23,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Publishing; using Umbraco.Core.Macros; +using Umbraco.Core.Manifest; using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Core.Strings; @@ -259,8 +262,12 @@ namespace Umbraco.Core ///
protected virtual void InitializeResolvers() { - PropertyEditorResolver.Current = new PropertyEditorResolver(() => PluginManager.Current.ResolvePropertyEditors()); - ParameterEditorResolver.Current = new ParameterEditorResolver(() => PluginManager.Current.ResolveParameterEditors()); + var builder = new ManifestBuilder( + ApplicationCache.RuntimeCache, + new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), ApplicationCache.RuntimeCache)); + + PropertyEditorResolver.Current = new PropertyEditorResolver(() => PluginManager.Current.ResolvePropertyEditors(), builder); + ParameterEditorResolver.Current = new ParameterEditorResolver(() => PluginManager.Current.ResolveParameterEditors(), builder); //setup the validators resolver with our predefined validators ValidatorsResolver.Current = new ValidatorsResolver(new[] diff --git a/src/Umbraco.Core/Manifest/ManifestBuilder.cs b/src/Umbraco.Core/Manifest/ManifestBuilder.cs index d62c215c8d..5f95deea22 100644 --- a/src/Umbraco.Core/Manifest/ManifestBuilder.cs +++ b/src/Umbraco.Core/Manifest/ManifestBuilder.cs @@ -1,6 +1,8 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.PropertyEditors; @@ -11,73 +13,92 @@ namespace Umbraco.Core.Manifest ///
internal class ManifestBuilder { + private readonly IRuntimeCacheProvider _cache; + private readonly ManifestParser _parser; - private static readonly ConcurrentDictionary StaticCache = new ConcurrentDictionary(); - - private const string ManifestKey = "manifests"; + public ManifestBuilder(IRuntimeCacheProvider cache, ManifestParser parser) + { + _cache = cache; + _parser = parser; + } + + private const string GridEditorsKey = "grideditors"; private const string PropertyEditorsKey = "propertyeditors"; private const string ParameterEditorsKey = "parametereditors"; /// - /// Returns all property editors found in the manfifests + /// Returns all grid editors found in the manfifests /// - internal static IEnumerable PropertyEditors + internal IEnumerable GridEditors { get { - return (IEnumerable) StaticCache.GetOrAdd( - PropertyEditorsKey, - s => + return _cache.GetCacheItem>( + typeof (ManifestBuilder) + GridEditorsKey, + () => + { + var editors = new List(); + foreach (var manifest in _parser.GetManifests()) { - var editors = new List(); - foreach (var manifest in GetManifests()) + if (manifest.GridEditors != null) { - if (manifest.PropertyEditors != null) - { - editors.AddRange(ManifestParser.GetPropertyEditors(manifest.PropertyEditors)); - } - + editors.AddRange(ManifestParser.GetGridEditors(manifest.GridEditors)); } - return editors; - }); + + } + return editors; + }, new TimeSpan(0, 10, 0)); + } + } + + /// + /// Returns all property editors found in the manfifests + /// + internal IEnumerable PropertyEditors + { + get + { + return _cache.GetCacheItem>( + typeof(ManifestBuilder) + PropertyEditorsKey, + () => + { + var editors = new List(); + foreach (var manifest in _parser.GetManifests()) + { + if (manifest.PropertyEditors != null) + { + editors.AddRange(ManifestParser.GetPropertyEditors(manifest.PropertyEditors)); + } + + } + return editors; + }, new TimeSpan(0, 10, 0)); } } /// /// Returns all parameter editors found in the manfifests and all property editors that are flagged to be parameter editors /// - internal static IEnumerable ParameterEditors + internal IEnumerable ParameterEditors { get { - return (IEnumerable)StaticCache.GetOrAdd( - ParameterEditorsKey, - s => + return _cache.GetCacheItem>( + typeof (ManifestBuilder) + ParameterEditorsKey, + () => { var editors = new List(); - foreach (var manifest in GetManifests()) + foreach (var manifest in _parser.GetManifests()) { if (manifest.ParameterEditors != null) { - editors.AddRange(ManifestParser.GetParameterEditors(manifest.ParameterEditors)); + editors.AddRange(ManifestParser.GetParameterEditors(manifest.ParameterEditors)); } } return editors; - }); + }, new TimeSpan(0, 10, 0)); } } - - /// - /// Ensures the manifests are found and loaded into memory - /// - private static IEnumerable GetManifests() - { - return (IEnumerable) StaticCache.GetOrAdd(ManifestKey, s => - { - var parser = new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins"))); - return parser.GetManifests(); - }); - } - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index 6794377001..7847eccf6e 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.PropertyEditors; @@ -17,15 +18,28 @@ namespace Umbraco.Core.Manifest internal class ManifestParser { private readonly DirectoryInfo _pluginsDir; - + private readonly IRuntimeCacheProvider _cache; + //used to strip comments private static readonly Regex CommentsSurround = new Regex(@"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", RegexOptions.Compiled); private static readonly Regex CommentsLine = new Regex(@"^\s*//.*?$", RegexOptions.Compiled | RegexOptions.Multiline); - public ManifestParser(DirectoryInfo pluginsDir) + public ManifestParser(DirectoryInfo pluginsDir, IRuntimeCacheProvider cache) { if (pluginsDir == null) throw new ArgumentNullException("pluginsDir"); _pluginsDir = pluginsDir; + _cache = cache; + } + + /// + /// Parse the grid editors from the json array + /// + /// + /// + internal static IEnumerable GetGridEditors(JArray jsonEditors) + { + return JsonConvert.DeserializeObject>( + jsonEditors.ToString()); } /// @@ -57,11 +71,17 @@ namespace Umbraco.Core.Manifest /// Get all registered manifests /// /// + /// + /// This ensures that we only build and look for all manifests once per Web app (based on the IRuntimeCache) + /// public IEnumerable GetManifests() { - //get all Manifest.js files in the appropriate folders - var manifestFileContents = GetAllManifestFileContents(_pluginsDir); - return CreateManifests(manifestFileContents.ToArray()); + return _cache.GetCacheItem>(typeof (ManifestParser) + "GetManifests", () => + { + //get all Manifest.js files in the appropriate folders + var manifestFileContents = GetAllManifestFileContents(_pluginsDir); + return CreateManifests(manifestFileContents.ToArray()); + }, new TimeSpan(0, 10, 0)); } /// @@ -154,6 +174,20 @@ namespace Umbraco.Core.Manifest throw new FormatException("The manifest is not formatted correctly contains more than one 'propertyEditors' element"); } + //validate the parameterEditors section + var paramEditors = deserialized.Properties().Where(x => x.Name == "parameterEditors").ToArray(); + if (paramEditors.Length > 1) + { + throw new FormatException("The manifest is not formatted correctly contains more than one 'parameterEditors' element"); + } + + //validate the gridEditors section + var gridEditors = deserialized.Properties().Where(x => x.Name == "gridEditors").ToArray(); + if (gridEditors.Length > 1) + { + throw new FormatException("The manifest is not formatted correctly contains more than one 'gridEditors' element"); + } + var jConfig = init.Any() ? (JArray)deserialized["javascript"] : new JArray(); ReplaceVirtualPaths(jConfig); @@ -181,7 +215,8 @@ namespace Umbraco.Core.Manifest JavaScriptInitialize = jConfig, StylesheetInitialize = cssConfig, PropertyEditors = propEditors.Any() ? (JArray)deserialized["propertyEditors"] : new JArray(), - ParameterEditors = propEditors.Any() ? (JArray)deserialized["parameterEditors"] : new JArray() + ParameterEditors = propEditors.Any() ? (JArray)deserialized["parameterEditors"] : new JArray(), + GridEditors = propEditors.Any() ? (JArray)deserialized["gridEditors"] : new JArray() }; result.Add(manifest); } diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index 3475a60312..dea1eb9877 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -26,5 +26,10 @@ namespace Umbraco.Core.Manifest /// The json array of parameter editors /// public JArray ParameterEditors { get; set; } + + /// + /// The json array of grid editors + /// + public JArray GridEditors { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/GridEditor.cs b/src/Umbraco.Core/PropertyEditors/GridEditor.cs new file mode 100644 index 0000000000..2fd24a2e99 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/GridEditor.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.PropertyEditors +{ + internal class GridEditor + { + public GridEditor() + { + Config = new Dictionary(); + } + + [JsonProperty("name", Required = Required.Always)] + public string Name { get; set; } + + [JsonProperty("alias", Required = Required.Always)] + public string Alias { get; set; } + + [JsonProperty("view", Required = Required.Always)] + public string View { get; set; } + + [JsonProperty("render")] + public string Render { get; set; } + + [JsonProperty("icon", Required = Required.Always)] + public string Icon { get; set; } + + [JsonProperty("config")] + public IDictionary Config { get; set; } + + protected bool Equals(GridEditor other) + { + return string.Equals(Alias, other.Alias); + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// + /// true if the specified object is equal to the current object; otherwise, false. + /// + /// The object to compare with the current object. + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((GridEditor) obj); + } + + /// + /// Serves as a hash function for a particular type. + /// + /// + /// A hash code for the current . + /// + public override int GetHashCode() + { + return Alias.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs index ef01b3a891..2130476485 100644 --- a/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/ParameterEditorResolver.cs @@ -15,11 +15,14 @@ namespace Umbraco.Core.PropertyEditors /// internal class ParameterEditorResolver : LazyManyObjectsResolverBase { - public ParameterEditorResolver(Func> typeListProducerList) + private readonly ManifestBuilder _builder; + + public ParameterEditorResolver(Func> typeListProducerList, ManifestBuilder builder) : base(typeListProducerList, ObjectLifetimeScope.Application) { + _builder = builder; } - + /// /// Returns the parameter editors /// @@ -38,9 +41,9 @@ namespace Umbraco.Core.PropertyEditors //exclude the non parameter editor c# property editors .Except(filtered) //include the manifest parameter editors - .Union(ManifestBuilder.ParameterEditors) + .Union(_builder.ParameterEditors) //include the manifest prop editors that are parameter editors - .Union(ManifestBuilder.PropertyEditors.Where(x => x.IsParameterEditor)); + .Union(_builder.PropertyEditors.Where(x => x.IsParameterEditor)); } } diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs index 90c6e184aa..4e17915712 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using Umbraco.Core.IO; using Umbraco.Core.Manifest; using Umbraco.Core.ObjectResolution; @@ -17,7 +19,17 @@ namespace Umbraco.Core.PropertyEditors public PropertyEditorResolver(Func> typeListProducerList) : base(typeListProducerList, ObjectLifetimeScope.Application) { - _unioned = new Lazy>(() => Values.Union(ManifestBuilder.PropertyEditors).ToList()); + var builder = new ManifestBuilder( + ApplicationContext.Current.ApplicationCache.RuntimeCache, + new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), ApplicationContext.Current.ApplicationCache.RuntimeCache)); + + _unioned = new Lazy>(() => Values.Union(builder.PropertyEditors).ToList()); + } + + internal PropertyEditorResolver(Func> typeListProducerList, ManifestBuilder builder) + : base(typeListProducerList, ObjectLifetimeScope.Application) + { + _unioned = new Lazy>(() => Values.Union(builder.PropertyEditors).ToList()); } private readonly Lazy> _unioned; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d9d37cb436..dec8a9c1fc 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -347,6 +347,7 @@ + diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index b51d5b84c9..603ff56838 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -63,6 +63,61 @@ namespace Umbraco.Tests.Manifest Assert.AreEqual(0, parser.ElementAt(0).PreValueEditor.Fields.ElementAt(1).Validators.Count()); } + [Test] + public void Parse_Grid_Editors() + { + var a = JsonConvert.DeserializeObject(@"[ + { + alias: 'Test.Test1', + name: 'Test 1', + view: 'blah', + icon: 'hello' + }, + { + alias: 'Test.Test2', + name: 'Test 2', + config: { key1: 'some default val' }, + view: '/hello/world.cshtml', + icon: 'helloworld' + }, + { + alias: 'Test.Test3', + name: 'Test 3', + config: { key1: 'some default val' }, + view: '/hello/world.html', + render: '/hello/world.cshtml', + icon: 'helloworld' + } +]"); + var parser = ManifestParser.GetGridEditors(a).ToArray(); + + Assert.AreEqual(3, parser.Count()); + + Assert.AreEqual("Test.Test1", parser.ElementAt(0).Alias); + Assert.AreEqual("Test 1", parser.ElementAt(0).Name); + Assert.AreEqual("blah", parser.ElementAt(0).View); + Assert.AreEqual("hello", parser.ElementAt(0).Icon); + Assert.IsNull(parser.ElementAt(0).Render); + Assert.AreEqual(0, parser.ElementAt(0).Config.Count); + + Assert.AreEqual("Test.Test2", parser.ElementAt(1).Alias); + Assert.AreEqual("Test 2", parser.ElementAt(1).Name); + Assert.AreEqual("/hello/world.cshtml", parser.ElementAt(1).View); + Assert.AreEqual("helloworld", parser.ElementAt(1).Icon); + Assert.IsNull(parser.ElementAt(1).Render); + Assert.AreEqual(1, parser.ElementAt(1).Config.Count); + Assert.AreEqual("some default val", parser.ElementAt(1).Config["key1"]); + + Assert.AreEqual("Test.Test3", parser.ElementAt(2).Alias); + Assert.AreEqual("Test 3", parser.ElementAt(2).Name); + Assert.AreEqual("/hello/world.html", parser.ElementAt(2).View); + Assert.AreEqual("helloworld", parser.ElementAt(2).Icon); + Assert.AreEqual("/hello/world.cshtml", parser.ElementAt(2).Render); + Assert.AreEqual(1, parser.ElementAt(2).Config.Count); + Assert.AreEqual("some default val", parser.ElementAt(2).Config["key1"]); + + } + [Test] public void Parse_Property_Editors() { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/grid.service.js b/src/Umbraco.Web.UI.Client/src/common/services/grid.service.js index f64701b8d8..898d1293fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/grid.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/grid.service.js @@ -1,7 +1,7 @@ angular.module('umbraco.services') .factory('gridService', function ($http, $q){ - var configPath = "../config/grid.editors.config.js"; + var configPath = Umbraco.Sys.ServerVariables.umbracoUrls.gridConfig; var service = { getGridEditors: function () { return $http.get(configPath); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 003104de63..37fbd32e22 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -14,7 +14,10 @@ using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Manifest; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -31,6 +34,7 @@ namespace Umbraco.Web.Editors /// A controller to render out the default back office view and JS results ///
[UmbracoUseHttps] + [DisableClientCache] public class BackOfficeController : UmbracoController { /// @@ -81,11 +85,11 @@ namespace Umbraco.Web.Editors /// /// [MinifyJavaScriptResult(Order = 0)] - [OutputCache(Order = 1, VaryByParam = "none", Location = OutputCacheLocation.Any, Duration = 5000)] + [OutputCache(Order = 1, VaryByParam = "none", Location = OutputCacheLocation.Server, Duration = 5000)] public JavaScriptResult Application() { var plugins = new DirectoryInfo(Server.MapPath("~/App_Plugins")); - var parser = new ManifestParser(plugins); + var parser = new ManifestParser(plugins, ApplicationContext.ApplicationCache.RuntimeCache); var initJs = new JsInitialization(parser); var initCss = new CssInitialization(parser); @@ -106,16 +110,75 @@ namespace Umbraco.Web.Editors [HttpGet] public JsonNetResult GetManifestAssetList() { - var plugins = new DirectoryInfo(Server.MapPath("~/App_Plugins")); - var parser = new ManifestParser(plugins); - var initJs = new JsInitialization(parser); - var initCss = new CssInitialization(parser); - var jsResult = initJs.GetJavascriptInitializationArray(HttpContext, new JArray()); - var cssResult = initCss.GetStylesheetInitializationArray(HttpContext); + Func getResult = () => + { + var plugins = new DirectoryInfo(Server.MapPath("~/App_Plugins")); + var parser = new ManifestParser(plugins, ApplicationContext.ApplicationCache.RuntimeCache); + var initJs = new JsInitialization(parser); + var initCss = new CssInitialization(parser); + var jsResult = initJs.GetJavascriptInitializationArray(HttpContext, new JArray()); + var cssResult = initCss.GetStylesheetInitializationArray(HttpContext); + ManifestParser.MergeJArrays(jsResult, cssResult); + return jsResult; + }; - ManifestParser.MergeJArrays(jsResult, cssResult); + //cache the result if debugging is disabled + var result = HttpContext.IsDebuggingEnabled + ? getResult() + : ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem( + typeof (BackOfficeController) + "GetManifestAssetList", + () => getResult(), + new TimeSpan(0, 10, 0)); - return new JsonNetResult {Data = jsResult, Formatting = Formatting.Indented}; + return new JsonNetResult { Data = result, Formatting = Formatting.Indented }; + } + + [UmbracoAuthorize(Order = 0)] + [HttpGet] + public JsonNetResult GetGridConfig() + { + Func> getResult = () => + { + var editors = new List(); + var gridConfig = Server.MapPath("~/Config/grid.editors.config.js"); + if (System.IO.File.Exists(gridConfig)) + { + try + { + var arr = JArray.Parse(System.IO.File.ReadAllText(gridConfig)); + //ensure the contents parse correctly to objects + var parsed = ManifestParser.GetGridEditors(arr); + editors.AddRange(parsed); + } + catch (Exception ex) + { + LogHelper.Error("Could not parse the contents of grid.editors.config.js into a JSON array", ex); + } + } + + var plugins = new DirectoryInfo(Server.MapPath("~/App_Plugins")); + var parser = new ManifestParser(plugins, ApplicationContext.ApplicationCache.RuntimeCache); + var builder = new ManifestBuilder(ApplicationContext.ApplicationCache.RuntimeCache, parser); + foreach (var gridEditor in builder.GridEditors) + { + //no duplicates! (based on alias) + if (editors.Contains(gridEditor) == false) + { + editors.Add(gridEditor); + } + } + return editors; + }; + + //cache the result if debugging is disabled + var result = HttpContext.IsDebuggingEnabled + ? getResult() + : ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem>( + typeof(BackOfficeController) + "GetGridConfig", + () => getResult(), + new TimeSpan(0, 10, 0)); + + return new JsonNetResult { Data = result, Formatting = Formatting.Indented }; } /// @@ -124,168 +187,172 @@ namespace Umbraco.Web.Editors /// [UmbracoAuthorize(Order = 0)] [MinifyJavaScriptResult(Order = 1)] - [OutputCache(Order = 2, VaryByParam = "none", Location = OutputCacheLocation.Any, Duration = 5000)] public JavaScriptResult ServerVariables() { - //authenticationApiBaseUrl - - //now we need to build up the variables - var d = new Dictionary + Func> getResult = () => new Dictionary + { { + "umbracoUrls", new Dictionary { - "umbracoUrls", new Dictionary - { - {"legacyTreeJs", Url.Action("LegacyTreeJs", "BackOffice")}, - {"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")}, - {"serverVarsJs", Url.Action("Application", "BackOffice")}, - //API URLs - { - "embedApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetEmbed("",0,0)) - }, - { - "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.PostDisableUser(0)) - }, - { - "contentApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.PostSave(null)) - }, - { - "mediaApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetRootMedia()) - }, - { - "imagesApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetBigThumbnail(0)) - }, - { - "sectionApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetSections()) - }, - { - "treeApplicationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetApplicationTrees(null, null, null)) - }, - { - "contentTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllowedChildren(0)) - }, - { - "mediaTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllowedChildren(0)) - }, - { - "macroApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetMacroParameters(0)) - }, - { - "authenticationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.PostLogin(null)) - }, - { - "currentUserApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetMembershipProviderConfig()) - }, - { - "legacyApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.DeleteLegacyItem(null, null, null)) - }, - { - "entityApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetById(0, UmbracoEntityTypes.Media)) - }, - { - "dataTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetById(0)) - }, - { - "dashboardApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetDashboard(null)) - }, - { - "logApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetEntityLog(0)) - }, - { - "memberApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetByKey(Guid.Empty)) - }, - { - "packageInstallApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.Fetch(string.Empty)) - }, - { - "rteApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetConfiguration()) - }, - { - "stylesheetApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAll()) - }, - { - "memberTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllTypes()) - }, - { - "updateCheckApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetCheck()) - }, - { - "tagApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllTags(null)) - }, - { - "memberTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetNodes("-1", null)) - }, - { - "mediaTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetNodes("-1", null)) - }, - { - "contentTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetNodes("-1", null)) - }, - { - "tagsDataBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetTags("")) - }, - { - "examineMgmtBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetIndexerDetails()) - }, - { - "xmlDataIntegrityBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.CheckContentXmlTable()) - } - } - }, + {"legacyTreeJs", Url.Action("LegacyTreeJs", "BackOffice")}, + {"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")}, + {"gridConfig", Url.Action("GetGridConfig", "BackOffice")}, + {"serverVarsJs", Url.Action("Application", "BackOffice")}, + //API URLs + { + "embedApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetEmbed("", 0, 0)) + }, + { + "userApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.PostDisableUser(0)) + }, + { + "contentApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.PostSave(null)) + }, + { + "mediaApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetRootMedia()) + }, + { + "imagesApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetBigThumbnail(0)) + }, + { + "sectionApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetSections()) + }, + { + "treeApplicationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetApplicationTrees(null, null, null)) + }, + { + "contentTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllowedChildren(0)) + }, + { + "mediaTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllowedChildren(0)) + }, + { + "macroApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetMacroParameters(0)) + }, + { + "authenticationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.PostLogin(null)) + }, + { + "currentUserApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetMembershipProviderConfig()) + }, + { + "legacyApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.DeleteLegacyItem(null, null, null)) + }, + { + "entityApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0, UmbracoEntityTypes.Media)) + }, + { + "dataTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0)) + }, + { + "dashboardApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetDashboard(null)) + }, + { + "logApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetEntityLog(0)) + }, + { + "memberApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetByKey(Guid.Empty)) + }, + { + "packageInstallApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.Fetch(string.Empty)) + }, + { + "rteApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetConfiguration()) + }, + { + "stylesheetApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAll()) + }, + { + "memberTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllTypes()) + }, + { + "updateCheckApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetCheck()) + }, + { + "tagApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllTags(null)) + }, + { + "memberTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetNodes("-1", null)) + }, + { + "mediaTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetNodes("-1", null)) + }, + { + "contentTreeBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetNodes("-1", null)) + }, + { + "tagsDataBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetTags("")) + }, + { + "examineMgmtBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetIndexerDetails()) + }, + { + "xmlDataIntegrityBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.CheckContentXmlTable()) + } + } + }, + { + "umbracoSettings", new Dictionary { - "umbracoSettings", new Dictionary - { - {"umbracoPath", GlobalSettings.Path}, - {"mediaPath", IOHelper.ResolveUrl(SystemDirectories.Media).TrimEnd('/')}, - {"appPluginsPath", IOHelper.ResolveUrl(SystemDirectories.AppPlugins).TrimEnd('/')}, - { - "imageFileTypes", - string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes) - }, - {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, - } - }, + {"umbracoPath", GlobalSettings.Path}, + {"mediaPath", IOHelper.ResolveUrl(SystemDirectories.Media).TrimEnd('/')}, + {"appPluginsPath", IOHelper.ResolveUrl(SystemDirectories.AppPlugins).TrimEnd('/')}, + { + "imageFileTypes", + string.Join(",", UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes) + }, + {"keepUserLoggedIn", UmbracoConfig.For.UmbracoSettings().Security.KeepUserLoggedIn}, + } + }, + { + "umbracoPlugins", new Dictionary { - "umbracoPlugins", new Dictionary - { - {"trees", GetTreePluginsMetaData()} - } - }, - {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, - {"application", GetApplicationState()} - }; + {"trees", GetTreePluginsMetaData()} + } + }, + {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, + {"application", GetApplicationState()} + }; + //cache the result if debugging is disabled + var result = HttpContext.IsDebuggingEnabled + ? getResult() + : ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem>( + typeof(BackOfficeController) + "ServerVariables", + () => getResult(), + new TimeSpan(0, 10, 0)); - return JavaScript(ServerVariablesParser.Parse(d)); + return JavaScript(ServerVariablesParser.Parse(result)); } /// @@ -346,18 +413,30 @@ namespace Umbraco.Web.Editors /// [UmbracoAuthorize(Order = 0)] [MinifyJavaScriptResult(Order = 1)] - [OutputCache(Order = 2, VaryByParam = "none", Location = OutputCacheLocation.Any, Duration = 5000)] public JavaScriptResult LegacyTreeJs() - { - var javascript = new StringBuilder(); - javascript.AppendLine(LegacyTreeJavascript.GetLegacyTreeJavascript()); - javascript.AppendLine(LegacyTreeJavascript.GetLegacyIActionJavascript()); - //add all of the menu blocks - foreach (var file in GetLegacyActionJs(LegacyJsActionType.JsBlock)) + { + Func getResult = () => { - javascript.AppendLine(file); - } - return JavaScript(javascript.ToString()); + var javascript = new StringBuilder(); + javascript.AppendLine(LegacyTreeJavascript.GetLegacyTreeJavascript()); + javascript.AppendLine(LegacyTreeJavascript.GetLegacyIActionJavascript()); + //add all of the menu blocks + foreach (var file in GetLegacyActionJs(LegacyJsActionType.JsBlock)) + { + javascript.AppendLine(file); + } + return javascript.ToString(); + }; + + //cache the result if debugging is disabled + var result = HttpContext.IsDebuggingEnabled + ? getResult() + : ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem( + typeof(BackOfficeController) + "LegacyTreeJs", + () => getResult(), + new TimeSpan(0, 10, 0)); + + return JavaScript(result); } /// diff --git a/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs new file mode 100644 index 0000000000..448e04222c --- /dev/null +++ b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Web; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// Will ensure that client-side cache does not occur by sending the correct response headers + /// + public class DisableClientCacheAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext filterContext) + { + if (filterContext.IsChildAction) base.OnResultExecuting(filterContext); + + filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); + filterContext.HttpContext.Response.Cache.SetValidUntilExpires(false); + filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); + filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); + filterContext.HttpContext.Response.Cache.SetNoStore(); + + base.OnResultExecuting(filterContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs index 498c6acc76..8281af7412 100644 --- a/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs +++ b/src/Umbraco.Web/Mvc/MinifyJavaScriptResultAttribute.cs @@ -1,5 +1,4 @@ -using System.Web; -using System.Web.Mvc; +using System.Web.Mvc; using System.Web.UI; using ClientDependency.Core; using ClientDependency.Core.CompositeFiles; @@ -7,7 +6,7 @@ using ClientDependency.Core.CompositeFiles; namespace Umbraco.Web.Mvc { /// - /// Minifies and caches the result for the JavaScriptResult + /// Minifies the result for the JavaScriptResult /// /// /// Only minifies in release mode From b9082cf39017208affa99227b5850660295d07ad Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 14:00:48 +1100 Subject: [PATCH 071/249] Fixes our usages of GetCacheItem so that there is a timeout, this is needed otherwise memory won't be given back unless memory gets too high where in fact for most of these we want to give it back as soon as possible if it isn't being used. --- src/Umbraco.Core/Services/ApplicationTreeService.cs | 2 +- src/Umbraco.Core/Services/SectionService.cs | 2 +- src/Umbraco.Web/Security/MembershipHelper.cs | 10 +++++----- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/ApplicationTreeService.cs b/src/Umbraco.Core/Services/ApplicationTreeService.cs index 5278990d6a..757c8fb8fe 100644 --- a/src/Umbraco.Core/Services/ApplicationTreeService.cs +++ b/src/Umbraco.Core/Services/ApplicationTreeService.cs @@ -128,7 +128,7 @@ namespace Umbraco.Core.Services return list; - }); + }, new TimeSpan(0, 10, 0)); } /// diff --git a/src/Umbraco.Core/Services/SectionService.cs b/src/Umbraco.Core/Services/SectionService.cs index fd9519bc1c..b30c63fc48 100644 --- a/src/Umbraco.Core/Services/SectionService.cs +++ b/src/Umbraco.Core/Services/SectionService.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Services return list; - }); + }, new TimeSpan(0, 10, 0)); } internal void LoadXml(Func callback, bool saveAfterCallbackIfChanged) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index f066476948..d35f343f64 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -234,7 +234,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByProviderKey(key); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + }, new TimeSpan(0, 5, 0)); } public IPublishedContent GetById(int memberId) @@ -250,7 +250,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetById(memberId); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + }, new TimeSpan(0, 5, 0)); } public IPublishedContent GetByUsername(string username) @@ -266,7 +266,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByUsername(username); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + }, new TimeSpan(0, 5, 0)); } public IPublishedContent GetByEmail(string email) @@ -282,7 +282,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByEmail(email); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }); + }, new TimeSpan(0, 5, 0)); } /// @@ -827,7 +827,7 @@ namespace Umbraco.Web.Security var username = provider.GetCurrentUserName(); var member = _applicationContext.Services.MemberService.GetByUsername(username); return member; - }); + }, new TimeSpan(0, 5, 0)); } private string GetCacheKey(string key, params object[] additional) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ab3a8a47ad..e078afc819 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -271,6 +271,7 @@ + From e5cf7b089cee6efd418dc09da6326638c46e142e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 14:47:08 +1100 Subject: [PATCH 072/249] oops, added timeouts where they cannot exist for request cache, fixes build --- src/Umbraco.Web/Security/MembershipHelper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index d35f343f64..f066476948 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -234,7 +234,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByProviderKey(key); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }, new TimeSpan(0, 5, 0)); + }); } public IPublishedContent GetById(int memberId) @@ -250,7 +250,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetById(memberId); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }, new TimeSpan(0, 5, 0)); + }); } public IPublishedContent GetByUsername(string username) @@ -266,7 +266,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByUsername(username); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }, new TimeSpan(0, 5, 0)); + }); } public IPublishedContent GetByEmail(string email) @@ -282,7 +282,7 @@ namespace Umbraco.Web.Security var result = _applicationContext.Services.MemberService.GetByEmail(email); return result == null ? null : new MemberPublishedContent(result).CreateModel(); - }, new TimeSpan(0, 5, 0)); + }); } /// @@ -827,7 +827,7 @@ namespace Umbraco.Web.Security var username = provider.GetCurrentUserName(); var member = _applicationContext.Services.MemberService.GetByUsername(username); return member; - }, new TimeSpan(0, 5, 0)); + }); } private string GetCacheKey(string key, params object[] additional) From 85ba41dd06dc0612d4f0906b63a865edc24b40cb Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 15:50:58 +1100 Subject: [PATCH 073/249] Fixes: U4-5435 Dropdown list multiple, publish keys - single item --- .../PropertyEditors/PublishValuesMultipleValueEditor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs b/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs index eb746405b2..3d68936118 100644 --- a/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/PublishValuesMultipleValueEditor.cs @@ -39,14 +39,17 @@ namespace Umbraco.Web.PropertyEditors /// public override string ConvertDbToString(Property property, PropertyType propertyType, IDataTypeService dataTypeService) { + //publishing ids, so just need to return the value as-is if (_publishIds) { - return base.ConvertDbToString(property, propertyType, dataTypeService); + return property.Value.ToString(); } + //get the multiple ids var selectedIds = property.Value.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); if (selectedIds.Any() == false) { + //nothing there return base.ConvertDbToString(property, propertyType, dataTypeService); } From cbef51eaf0cfe9db94335177c8f854b99f69f7f1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 16:10:46 +1100 Subject: [PATCH 074/249] Fixes: U4-6294 Feature Request 7.2.1 Grid - UX - labeling the grid editors - but will see if we can do a better job of the labeling --- .../src/views/propertyeditors/grid/grid.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index cd3e504156..0155bc2e3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -73,7 +73,7 @@ - + @@ -82,7 +82,7 @@ ng-mouseover="setInfohighlightRow(row)" ng-mouseleave="disableInfohighlightRow(row)" href> - + @@ -92,7 +92,7 @@ ng-mouseleave="disableInfohighlightRow(row)" ng-click="editGridItemSettings(row, 'row')" href> - + @@ -118,7 +118,7 @@ ng-mouseover="setCurrentControl(control)" ng-mouseleave="disableCurrentControl(control)" ng-animate="'fade'" - class="usky-control"> + class="usky-control" title="{{control.editor.name}}">
- +
@@ -149,7 +149,7 @@ ng-mouseover="setInfohighlightControl(control)" ng-mouseleave="disableInfohighlightControl(control)" href> - + @@ -160,7 +160,7 @@ ng-mouseleave="disableInfohighlightArea(area)" ng-click="editGridItemSettings(area, 'cell')" href> - + @@ -192,7 +192,7 @@ ng-animate="'fade'"> - +
From af112f6b20dae0fab4a0dad6744e00240eb25cfc Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 17:08:14 +1100 Subject: [PATCH 075/249] Adds nicer grid editor labels on hover --- .../src/less/gridview.less | 33 ++++++++++++++++--- .../src/views/propertyeditors/grid/grid.html | 3 +- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 107e042a16..0f0d09a7e9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -251,10 +251,6 @@ IFRAME {overflow:hidden;} border: 1px dashed @blue !important; } -.usky-grid .usky-control-inner{ - min-height: 60px; -} - // CONTROL PLACEHOLDER @@ -524,11 +520,38 @@ IFRAME {overflow:hidden;} border: 1px dashed transparent; } - .usky-grid .usky-control-inner{ + .usky-grid .usky-control-inner { padding: 5px; margin-right: 45px; margin-bottom: 15px; border: 1px dashed transparent; + min-height: 60px; + position:relative; + } + + .usky-grid .usky-control-inner > ins { + position: absolute; + top: -22px; + left: 0; + text-decoration: none; + padding: 0px 7px; + /*opacity: .8;*/ + display:none; + font-size:0.8em; + background-color:@blueLight; + -moz-border-radius: 0px; + -webkit-border-radius: 5px 5px 0px 0px; + border-radius: 5px 5px 0px 0px; + } + + .usky-grid .usky-control-inner:hover + { + border:@grayLighter solid 1px; + } + + .usky-grid .usky-control-inner:hover > ins { + display:block; + z-index:100000; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 0155bc2e3f..1cddb13855 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -118,7 +118,7 @@ ng-mouseover="setCurrentControl(control)" ng-mouseleave="disableCurrentControl(control)" ng-animate="'fade'" - class="usky-control" title="{{control.editor.name}}"> + class="usky-control">
+ {{control.editor.name}}
Date: Thu, 19 Mar 2015 17:42:57 +1100 Subject: [PATCH 076/249] fixes build errors --- src/Umbraco.Core/CoreBootManager.cs | 6 ++--- src/Umbraco.Core/Models/Template.cs | 25 +++++++++---------- .../Services/LocalizedTextService.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 19545987e9..81e87c645d 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -303,12 +303,10 @@ namespace Umbraco.Core { var builder = new ManifestBuilder( ApplicationCache.RuntimeCache, - PropertyEditorResolver.Current = new PropertyEditorResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolvePropertyEditors()); - ParameterEditorResolver.Current = new ParameterEditorResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveParameterEditors()); new ManifestParser(new DirectoryInfo(IOHelper.MapPath("~/App_Plugins")), ApplicationCache.RuntimeCache)); - PropertyEditorResolver.Current = new PropertyEditorResolver(() => PluginManager.Current.ResolvePropertyEditors(), builder); - ParameterEditorResolver.Current = new ParameterEditorResolver(() => PluginManager.Current.ResolveParameterEditors(), builder); + PropertyEditorResolver.Current = new PropertyEditorResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolvePropertyEditors(), builder); + ParameterEditorResolver.Current = new ParameterEditorResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveParameterEditors(), builder); //setup the validators resolver with our predefined validators ValidatorsResolver.Current = new ValidatorsResolver( diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index 29744a89fa..bf49f9ca4d 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -142,27 +142,26 @@ namespace Umbraco.Core.Models public override object DeepClone() { - var clone = (Template)base.DeepClone() - //turn off change tracking - clone.DisableChangeTracking(); - //need to manually assign since they are readonly properties - clone._alias = Alias; - clone._name = Name; - //this shouldn't really be needed since we're not tracking - clone.ResetDirtyProperties(false); - //re-enable tracking - clone.EnableChangeTracking(); - - //We cannot call in to the base classes to clone because the base File class treats Alias, Name.. differently so we need to manually do the clone //Memberwise clone on Entity will work since it doesn't have any deep elements // for any sub class this will work for standard properties as well that aren't complex object's themselves. var clone = (Template)MemberwiseClone(); + //turn off change tracking + clone.DisableChangeTracking(); //Automatically deep clone ref properties that are IDeepCloneable - DeepCloneHelper.DeepCloneRefProperties(this, clone); + DeepCloneHelper.DeepCloneRefProperties(this, clone); + //TODO: This was part of the merge process, we need to test if this is required!! + ////need to manually assign since they are readonly properties + //clone._alias = Alias; + //clone._name = Name; + + //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); + //re-enable tracking + clone.EnableChangeTracking(); + return clone; } diff --git a/src/Umbraco.Core/Services/LocalizedTextService.cs b/src/Umbraco.Core/Services/LocalizedTextService.cs index a499ea7640..51c4d5dc1d 100644 --- a/src/Umbraco.Core/Services/LocalizedTextService.cs +++ b/src/Umbraco.Core/Services/LocalizedTextService.cs @@ -229,7 +229,7 @@ namespace Umbraco.Core.Services return "[" + key + "]"; } - private static string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) + private string GetFromXmlSource(IDictionary> xmlSource, CultureInfo culture, string area, string key, IDictionary tokens) { if (xmlSource.ContainsKey(culture) == false) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 3194f60b12..8f581d6e45 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -275,7 +275,6 @@ - From ab4cb08c7d3b28659b759e304dc456130936fb0f Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 17:48:40 +1100 Subject: [PATCH 077/249] removed notes after checking --- src/Umbraco.Core/Models/Template.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Core/Models/Template.cs b/src/Umbraco.Core/Models/Template.cs index bf49f9ca4d..3a8cbf8794 100644 --- a/src/Umbraco.Core/Models/Template.cs +++ b/src/Umbraco.Core/Models/Template.cs @@ -152,11 +152,6 @@ namespace Umbraco.Core.Models //Automatically deep clone ref properties that are IDeepCloneable DeepCloneHelper.DeepCloneRefProperties(this, clone); - //TODO: This was part of the merge process, we need to test if this is required!! - ////need to manually assign since they are readonly properties - //clone._alias = Alias; - //clone._name = Name; - //this shouldn't really be needed since we're not tracking clone.ResetDirtyProperties(false); //re-enable tracking From 09afb93ec3572ae537a257cc8682c712a28df890 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 19 Mar 2015 18:04:26 +1100 Subject: [PATCH 078/249] fixes test --- src/Umbraco.Tests/Plugins/TypeFinderTests.cs | 4 ++-- src/Umbraco.Web/Scheduling/LogScrubber.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index e9e8f9213b..576a21d5ac 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -79,8 +79,8 @@ namespace Umbraco.Tests.Plugins var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(6, typesFound.Count()); - Assert.AreEqual(6, originalTypesFound.Count()); + Assert.AreEqual(7, typesFound.Count()); + Assert.AreEqual(7, originalTypesFound.Count()); } [Test] diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index c4a5ce2c00..594c9233fd 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -67,7 +67,7 @@ namespace Umbraco.Web.Scheduling public override void PerformRun() { - using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) + using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); } From b714cbbd72dda64f658b164567f04ac934d346ac Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 19 Mar 2015 09:30:16 +0100 Subject: [PATCH 079/249] U4-6320 - fix log scrubbing intervals (from dev-v7) --- src/Umbraco.Web/Scheduling/LogScrubber.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 594c9233fd..63a3ebe186 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -34,9 +34,10 @@ namespace Umbraco.Web.Scheduling return new LogScrubber(this); } + // maximum age, in minutes private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { - int maximumAge = 24 * 60 * 60; + var maximumAge = 24 * 60; // 24 hours, in minutes try { if (settings.Logging.MaxLogAge > -1) @@ -44,15 +45,16 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 hours.", e); } return maximumAge; } + // scrubbing interval, in milliseconds public static int GetLogScrubbingInterval(IUmbracoSettingsSection settings) { - int interval = 24 * 60 * 60; //24 hours + var interval = 4 * 60 * 60 * 1000; // 4 hours, in milliseconds try { if (settings.Logging.CleaningMiliseconds > -1) @@ -60,7 +62,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 4 hours.", e); } return interval; } From 5ddcf42bc37362d09e0846578b1dcc0104f66bed Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 19 Mar 2015 09:37:39 +0100 Subject: [PATCH 080/249] U4-3753 - keep some things internal --- src/Umbraco.Web/Models/ContentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 4a535bb0e6..ae93f13c7b 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -35,7 +35,7 @@ namespace Umbraco.Web.Models /// The content path. /// The request Uri. /// The culture that would be selected to render the content. - public static CultureInfo GetCulture(UmbracoContext umbracoContext, ILocalizationService localizationService, + internal static CultureInfo GetCulture(UmbracoContext umbracoContext, ILocalizationService localizationService, int contentId, string contentPath, Uri current) { var route = umbracoContext.ContentCache.GetRouteById(contentId); // cached From dc7cb0882420ca413109a1e19c0b7f698f7334b4 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Thu, 19 Mar 2015 12:29:05 +0100 Subject: [PATCH 081/249] Styling for grid editors + row labels --- .../src/less/gridview.less | 60 +++++++++++-------- .../src/views/propertyeditors/grid/grid.html | 12 +++- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 0f0d09a7e9..3e9e9c0eda 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -251,6 +251,40 @@ IFRAME {overflow:hidden;} border: 1px dashed @blue !important; } +.usky-grid .warnhighlight > ins.item-label{border-color: @red;} +.usky-grid .infohighlight > ins.item-label{border-color: @blue;} + + +.usky-grid ins.item-label { + position: absolute; + top: -22px; + left: -1px; + text-decoration: none; + padding: 0px 7px; + display:none; + font-size:0.8em; + background-color: white; + color: @grayLight; + border: 1px dashed @grayLighter; + border-bottom: 1px solid white !important; + height: 20px; + overflow: hidden; +} + +.usky-grid .usky-row-inner > ins.item-label{ + top: -20px; + left: 0px; +} + +.usky-grid .usky-control-inner.selectedControl , .usky-grid .usky-row-inner.selectedRow{ + border: 1px dashed @grayLighter; + + > ins.item-label { + display: block; + z-index:100000; + } +} + // CONTROL PLACEHOLDER @@ -502,6 +536,7 @@ IFRAME {overflow:hidden;} /**************************************************************************************************/ .usky-grid .usky-cell{ + padding-top: 5px; padding-bottom: 15px; } @@ -524,35 +559,12 @@ IFRAME {overflow:hidden;} padding: 5px; margin-right: 45px; margin-bottom: 15px; + border: 1px dashed transparent; min-height: 60px; position:relative; } - .usky-grid .usky-control-inner > ins { - position: absolute; - top: -22px; - left: 0; - text-decoration: none; - padding: 0px 7px; - /*opacity: .8;*/ - display:none; - font-size:0.8em; - background-color:@blueLight; - -moz-border-radius: 0px; - -webkit-border-radius: 5px 5px 0px 0px; - border-radius: 5px 5px 0px 0px; - } - - .usky-grid .usky-control-inner:hover - { - border:@grayLighter solid 1px; - } - - .usky-grid .usky-control-inner:hover > ins { - display:block; - z-index:100000; - } /**************************************************************************************************/ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 1cddb13855..05809cad02 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -100,8 +100,14 @@
+ ng-class="{last:$last, + first:$first, + warnhighlight:currentWarnhighlightRow == row, + infohighlight:currentInfohighlightRow == row, + selectedRow:currentRow == row && currentControl === null}"> + {{row.name}} +
@@ -170,7 +176,9 @@ infohighlight:currentInfohighlightControl == control, warnhighlight:currentWarnhighlightControl == control, selectedControl:currentControl == control}"> - {{control.editor.name}} + + {{control.editor.name}} +
Date: Thu, 19 Mar 2015 12:46:29 +0100 Subject: [PATCH 082/249] Tweaked borders a tiny bit --- src/Umbraco.Web.UI.Client/src/less/gridview.less | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 3e9e9c0eda..01879a6be3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -124,11 +124,12 @@ IFRAME {overflow:hidden;} border: 1px dashed rgba(0,0,0,0); } -.usky-grid .tb:hover .usky-row{ - border-bottom:1px dashed rgba(182, 182, 182, 0.3); +.umb-grid .tb:hover .usky-row{ + border-bottom:1px dashed rgba(182, 182, 182, 0) !important; } -/*.usky-grid .selectedRow { +/* +.usky-grid .selectedRow { border: 1px solid rgba(182, 182, 182, 0.3); }*/ @@ -235,7 +236,7 @@ IFRAME {overflow:hidden;} .usky-grid .usky-control:hover{ /*border: 1px solid rgba(182, 182, 182, 0.3);*/ - //border-bottom: 1px solid rgba(182, 182, 182, 0.3); + /*border-bottom: 1px solid rgba(182, 182, 182, 0.3);*/ } .usky-grid .usky-control-placeholder:hover{ @@ -265,7 +266,7 @@ IFRAME {overflow:hidden;} font-size:0.8em; background-color: white; color: @grayLight; - border: 1px dashed @grayLighter; + border: 1px dashed @grayLight; border-bottom: 1px solid white !important; height: 20px; overflow: hidden; @@ -277,7 +278,7 @@ IFRAME {overflow:hidden;} } .usky-grid .usky-control-inner.selectedControl , .usky-grid .usky-row-inner.selectedRow{ - border: 1px dashed @grayLighter; + border: 1px dashed @grayLight; > ins.item-label { display: block; From 311d503d8bd8695a7e065bbc282e853e18303a11 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 19 Mar 2015 12:53:43 +0100 Subject: [PATCH 083/249] Updates CDF dependency to latest version --- build/NuSpecs/UmbracoCms.Core.nuspec | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- src/Umbraco.Web.UI/packages.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/Umbraco.Web/packages.config | 2 +- src/umbraco.cms/packages.config | 2 +- src/umbraco.cms/umbraco.cms.csproj | 4 ++-- src/umbraco.controls/packages.config | 2 +- src/umbraco.controls/umbraco.controls.csproj | 4 ++-- src/umbraco.editorControls/packages.config | 2 +- src/umbraco.editorControls/umbraco.editorControls.csproj | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 27e2dea807..33c8a20953 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2015d0f2f6..ad23e7ae24 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -115,9 +115,9 @@ False ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - + False - ..\packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index bd2ee17ca4..ccf9ca3fa4 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e078afc819..82433abf49 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -106,7 +106,7 @@ False - ..\packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll False diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index f11d847bbf..51a9114dbb 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -1,7 +1,7 @@  - + diff --git a/src/umbraco.cms/packages.config b/src/umbraco.cms/packages.config index edda198298..bdaf00b95d 100644 --- a/src/umbraco.cms/packages.config +++ b/src/umbraco.cms/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/umbraco.cms/umbraco.cms.csproj b/src/umbraco.cms/umbraco.cms.csproj index 189ec04ccc..3a8a4cf536 100644 --- a/src/umbraco.cms/umbraco.cms.csproj +++ b/src/umbraco.cms/umbraco.cms.csproj @@ -106,9 +106,9 @@ false - + False - ..\packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll False diff --git a/src/umbraco.controls/packages.config b/src/umbraco.controls/packages.config index f2f0545321..0859f3d4e9 100644 --- a/src/umbraco.controls/packages.config +++ b/src/umbraco.controls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.controls/umbraco.controls.csproj b/src/umbraco.controls/umbraco.controls.csproj index 49ce586d75..a5c08e525a 100644 --- a/src/umbraco.controls/umbraco.controls.csproj +++ b/src/umbraco.controls/umbraco.controls.csproj @@ -68,9 +68,9 @@ false - + False - ..\packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll diff --git a/src/umbraco.editorControls/packages.config b/src/umbraco.editorControls/packages.config index f2f0545321..0859f3d4e9 100644 --- a/src/umbraco.editorControls/packages.config +++ b/src/umbraco.editorControls/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/umbraco.editorControls/umbraco.editorControls.csproj b/src/umbraco.editorControls/umbraco.editorControls.csproj index 426028b6dc..cfeb5f2103 100644 --- a/src/umbraco.editorControls/umbraco.editorControls.csproj +++ b/src/umbraco.editorControls/umbraco.editorControls.csproj @@ -116,7 +116,7 @@ False - ..\packages\ClientDependency.1.8.2.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll System From 19e4fe505950012e58588e682bb5f2df66dda541 Mon Sep 17 00:00:00 2001 From: Per Ploug Date: Thu, 19 Mar 2015 14:33:12 +0100 Subject: [PATCH 084/249] Grid labels header colors for delete and move actions --- src/Umbraco.Web.UI.Client/src/less/gridview.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/gridview.less b/src/Umbraco.Web.UI.Client/src/less/gridview.less index 01879a6be3..29a0ac9454 100644 --- a/src/Umbraco.Web.UI.Client/src/less/gridview.less +++ b/src/Umbraco.Web.UI.Client/src/less/gridview.less @@ -252,8 +252,8 @@ IFRAME {overflow:hidden;} border: 1px dashed @blue !important; } -.usky-grid .warnhighlight > ins.item-label{border-color: @red;} -.usky-grid .infohighlight > ins.item-label{border-color: @blue;} +.usky-grid .warnhighlight > ins.item-label{border-color: @red; color: @red;} +.usky-grid .infohighlight > ins.item-label{border-color: @blue; color: @blue;} .usky-grid ins.item-label { From 90fdbb007e7393cafc16754b012f560aa804b020 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 19 Mar 2015 14:45:00 +0100 Subject: [PATCH 085/249] Bump version --- build/UmbracoVersion.txt | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 0b0f35270e..a6175d96d2 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.3 \ No newline at end of file +7.2.4 \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index b5b9e206b5..cfde01fce6 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.2.3"); + private static readonly Version Version = new Version("7.2.4"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ad23e7ae24..829178c030 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2547,9 +2547,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7230 + 7240 / - http://localhost:7230 + http://localhost:7240 False False From fff0151b62536efaea5f1eb0093b25f5a946d0ee Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 09:31:14 +1100 Subject: [PATCH 086/249] Updates build process to: Ensure the AssemblyFileVersion is set to the real umbraco version, AssemblyInformationalVersion is set to the real umbraco version including the pre-release name if there is one, AssemblyCopyright has the correct year. --- build/Build.proj | 152 +++++++++++++++++++++++++------------------- src/SolutionInfo.cs | 9 ++- src/umbraco.sln | 1 + 3 files changed, 95 insertions(+), 67 deletions(-) diff --git a/build/Build.proj b/build/Build.proj index bd037b3569..40e24b6eb1 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -1,4 +1,3 @@ - - + - - - - + + + + - - - + + + @@ -161,7 +160,7 @@ - + @@ -177,14 +176,14 @@ - - - + + + - + @@ -217,7 +216,7 @@ - + @@ -226,24 +225,24 @@ DestinationFiles="$(WebAppFolder)Web.config" OverwriteReadOnlyFiles="true" SkipUnchangedFiles="false" /> - - - - + + + + - - - - - - - + + + + + + + @@ -261,20 +260,20 @@ - - + + - + - + - + @@ -315,12 +314,12 @@ - - $(BUILD_RELEASE) - $(BUILD_RELEASE)-$(BUILD_COMMENT) - - - + + $(BUILD_RELEASE) + $(BUILD_RELEASE)-$(BUILD_COMMENT) + + + - - + + + + + + + + + + + + \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 4ffe6a35dd..6ef556ffd3 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -1,8 +1,8 @@ -using System.Reflection; +using System.Reflection; using System.Resources; -[assembly: AssemblyCompany("umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2012")] +[assembly: AssemblyCompany("Umbraco")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2015")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -10,3 +10,6 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] + +[assembly: AssemblyFileVersion("7.2.4")] +[assembly: AssemblyInformationalVersion("7.2.4")] \ No newline at end of file diff --git a/src/umbraco.sln b/src/umbraco.sln index 70db6aa8fc..78202fd38a 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4 ..\build\BuildBelle.bat = ..\build\BuildBelle.bat ..\build\RevertToCleanInstall.bat = ..\build\RevertToCleanInstall.bat ..\build\RevertToEmptyInstall.bat = ..\build\RevertToEmptyInstall.bat + SolutionInfo.cs = SolutionInfo.cs umbraco.presentation.targets = umbraco.presentation.targets ..\build\UmbracoVersion.txt = ..\build\UmbracoVersion.txt ..\build\webpihash.txt = ..\build\webpihash.txt From b02e0c0232cdfb089c885ac3328b5660cc121d54 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 17:47:33 +1100 Subject: [PATCH 087/249] Fixes: U4-6433 Usercontrol loaded after package installation not executing --- .../src/views/common/legacy.controller.js | 25 ++++++++++++++----- .../developer/Packages/installer.aspx.cs | 4 +-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js index bf6bc287df..8e58535c74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js @@ -9,13 +9,26 @@ */ function LegacyController($scope, $routeParams, $element) { - var url = decodeURIComponent($routeParams.url.toLowerCase().trimStart("javascript:")); - var toClean = "*(){}[];:<>\\|'\""; - for (var i = 0; i < toClean.length; i++) { - var reg = new RegExp("\\" + toClean[i], "g"); - url = url.replace(reg, ""); + var url = decodeURIComponent($routeParams.url.toLowerCase().replace(/javascript\:/g, "")); + //split into path and query + var urlParts = url.split("?"); + var extIndex = urlParts[0].lastIndexOf("."); + var ext = extIndex === -1 ? "" : urlParts[0].substr(extIndex); + //path cannot be a js file + if (ext !== ".js" || ext === "") { + //path cannot contain any of these chars + var toClean = "*(){}[];:<>\\|'\""; + for (var i = 0; i < toClean.length; i++) { + var reg = new RegExp("\\" + toClean[i], "g"); + urlParts[0] = urlParts[0].replace(reg, ""); + } + //join cleaned path and query back together + url = urlParts[0] + (urlParts.length === 1 ? "" : ("?" + urlParts[1])); + $scope.legacyPath = url; + } + else { + throw "Invalid url"; } - $scope.legacyPath = url; } angular.module("umbraco").controller('Umbraco.LegacyController', LegacyController); \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs index 351015ddd8..6c75ab0e6e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs @@ -220,7 +220,7 @@ namespace umbraco.presentation.developer.packages var packageId = 0; int.TryParse(Request.GetItemAsString("pId"), out packageId); - switch (currentStep) + switch (currentStep.ToLowerInvariant()) { case "businesslogic": //first load in the config from the temporary directory @@ -241,7 +241,7 @@ namespace umbraco.presentation.developer.packages Response.Redirect("installer.aspx?installing=refresh&dir=" + dir + "&pId=" + packageId.ToString() + "&customUrl=" + Server.UrlEncode(_installer.Url)); } break; - case "customInstaller": + case "custominstaller": var customControl = Request.GetItemAsString("customControl"); if (customControl.IsNullOrWhiteSpace() == false) From 08a2ec6883df9dd969d60ade4768b5a7ce171f1c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 18:15:31 +1100 Subject: [PATCH 088/249] Fixes: U4-6436 Localization in the back office doesn't work immediately after app restart --- .../LocalizedTextServiceFileSources.cs | 138 ++++++++++-------- 1 file changed, 75 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index be53d8d16b..934b015079 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -20,12 +20,83 @@ namespace Umbraco.Core.Services //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( private readonly Dictionary _twoLetterCultureConverter = new Dictionary(); + private readonly Lazy>> _xmlSources; + public LocalizedTextServiceFileSources(IRuntimeCacheProvider cache, DirectoryInfo fileSourceFolder) { if (cache == null) throw new ArgumentNullException("cache"); if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder"); + _cache = cache; + //Create the lazy source for the _xmlSources + _xmlSources = new Lazy>>(() => + { + var result = new Dictionary>(); + + if (_fileSourceFolder == null) return result; + + foreach (var fileInfo in _fileSourceFolder.GetFiles("*.xml")) + { + var localCopy = fileInfo; + var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); + + //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct + // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we + // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that + // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) + // that any 4 letter file is named with the actual culture that it is! + CultureInfo culture = null; + if (filename.Length == 2) + { + //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't + //want to load in the entire doc into mem just to read a single value + using (var fs = fileInfo.OpenRead()) + using (var reader = XmlReader.Create(fs)) + { + if (reader.IsStartElement()) + { + if (reader.Name == "language") + { + if (reader.MoveToAttribute("culture")) + { + var cultureVal = reader.Value; + try + { + culture = CultureInfo.GetCultureInfo(cultureVal); + //add to the tracked dictionary + _twoLetterCultureConverter[filename] = culture; + } + catch (CultureNotFoundException) + { + LogHelper.Warn( + string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); + //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise + // an exception will be thrown. + } + } + } + } + } + } + if (culture == null) + { + culture = CultureInfo.GetCultureInfo(filename); + } + + //get the lazy value from cache + result.Add(culture, new Lazy(() => _cache.GetCacheItem( + string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () => + { + using (var fs = localCopy.OpenRead()) + { + return XDocument.Load(fs); + } + }, isSliding: true, timeout: TimeSpan.FromMinutes(10), dependentFiles: new[] { localCopy.FullName }))); + } + return result; + }); + if (fileSourceFolder.Exists == false) { LogHelper.Warn("The folder does not exist: {0}, therefore no sources will be discovered", () => fileSourceFolder.FullName); @@ -42,69 +113,7 @@ namespace Umbraco.Core.Services /// public IDictionary> GetXmlSources() { - var result = new Dictionary>(); - - if (_fileSourceFolder == null) return result; - - foreach (var fileInfo in _fileSourceFolder.GetFiles("*.xml")) - { - var localCopy = fileInfo; - var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); - - //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct - // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we - // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that - // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) - // that any 4 letter file is named with the actual culture that it is! - CultureInfo culture = null; - if (filename.Length == 2) - { - //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't - //want to load in the entire doc into mem just to read a single value - using (var fs = fileInfo.OpenRead()) - using (var reader = XmlReader.Create(fs)) - { - if (reader.IsStartElement()) - { - if (reader.Name == "language") - { - if (reader.MoveToAttribute("culture")) - { - var cultureVal = reader.Value; - try - { - culture = CultureInfo.GetCultureInfo(cultureVal); - //add to the tracked dictionary - _twoLetterCultureConverter[filename] = culture; - } - catch (CultureNotFoundException) - { - LogHelper.Warn( - string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); - //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise - // an exception will be thrown. - } - } - } - } - } - } - if (culture == null) - { - culture = CultureInfo.GetCultureInfo(filename); - } - - //get the lazy value from cache - result.Add(culture, new Lazy(() => _cache.GetCacheItem( - string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.Name), () => - { - using (var fs = localCopy.OpenRead()) - { - return XDocument.Load(fs); - } - }, isSliding: true, timeout: TimeSpan.FromMinutes(10), dependentFiles: new[] {localCopy.FullName}))); - } - return result; + return _xmlSources.Value; } //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( @@ -112,6 +121,9 @@ namespace Umbraco.Core.Services { if (twoLetterCulture.Length != 2) Attempt.Fail(); + //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized + var resolved = _xmlSources.Value; + return _twoLetterCultureConverter.ContainsKey(twoLetterCulture) ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture]) : Attempt.Fail(); From 8c4eba783782ed28d6b5f289928db82700d1a06c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 20 Mar 2015 10:16:53 +0100 Subject: [PATCH 089/249] "It's simple until you make it complicated" - So now this logic has been un-complicated: look for the web.config file, if it exists and has a umbracoConfigurationStatus appSetting then don't overwrite the web.config, the user is upgrading umbraco and config transforms can do their thing. Otherwise, copy in a fresh web.config with all the required Umbraco bits. --- build/NuSpecs/tools/install.ps1 | 86 ++++++--------------------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 1816aa8f75..9828d4e89c 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -47,85 +47,31 @@ if ($project) { robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log } - - $copyWebconfig = $false - # SJ - What can I say: big up for James Newton King for teaching us a hack for detecting if this is a new install vs. an upgrade! - # https://github.com/JamesNK/Newtonsoft.Json/pull/387 - would never have seen this without the controversial pull request.. - Try + $copyWebconfig = $true + $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" + + if(Test-Path $destinationWebConfig) { - # see if user is installing from VS NuGet console - # get reference to the window, the console host and the input history - # copy web.config if "install-package UmbracoCms" was last input - # this is in a try-catch as they might be using the regular NuGet dialog - # instead of package manager console - - $dte2 = Get-Interface $dte ([EnvDTE80.DTE2]) - - $consoleWindow = $(Get-VSComponentModel).GetService([NuGetConsole.IPowerConsoleWindow]) + Try + { + [xml]$config = Get-Content $destinationWebConfig - $props = $consoleWindow.GetType().GetProperties([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $prop = $props | ? { $_.Name -eq "ActiveHostInfo" } | select -first 1 - - $hostInfo = $prop.GetValue($consoleWindow) - - $history = $hostInfo.WpfConsole.InputHistory.History - - $lastCommand = $history | select -last 1 - - if ($lastCommand) - { - $lastCommand = $lastCommand.Trim().ToLower() - if ($lastCommand.StartsWith("install-package") -and $lastCommand.Contains("umbracocms")) - { - $copyWebconfig = $true + $config.configuration.appSettings.ChildNodes | ForEach-Object { + if($_.key -eq "umbracoConfigurationStatus") + { + # The web.config has an umbraco-specific appSetting in it + # don't overwrite it and let config transforms do their thing + $copyWebconfig = $false + } } - } + } + Catch { } } - Catch { } - - Try - { - # user is installing from VS NuGet dialog - # get reference to the window, then smart output console provider - # copy web.config if messages in buffered console contains "installing...UmbracoCms" in last operation - - $instanceField = [NuGet.Dialog.PackageManagerWindow].GetField("CurrentInstance", [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic) - $consoleField = [NuGet.Dialog.PackageManagerWindow].GetField("_smartOutputConsoleProvider", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $instance = $instanceField.GetValue($null) - - $consoleProvider = $consoleField.GetValue($instance) - - $console = $consoleProvider.CreateOutputConsole($false) - - $messagesField = $console.GetType().GetField("_messages", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $messages = $messagesField.GetValue($console) - - $operations = $messages -split "==============================" - - $lastOperation = $operations | select -last 1 - - if ($lastOperation) - { - $lastOperation = $lastOperation.ToLower() - $lines = $lastOperation -split "`r`n" - $installMatch = $lines | ? { $_.Contains("...umbracocms ") } | select -first 1 - - if ($installMatch) - { - $copyWebconfig = $true - } - } - } - Catch { } if($copyWebconfig -eq $true) { $packageWebConfigSource = Join-Path $rootPath "UmbracoFiles\Web.config" - $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" Copy-Item $packageWebConfigSource $destinationWebConfig -Force } From c5bbf97921aa127353baa253c20426df9621fe7e Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 19 Mar 2015 14:45:00 +0100 Subject: [PATCH 090/249] Bump version --- build/UmbracoVersion.txt | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 0b0f35270e..a6175d96d2 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.3 \ No newline at end of file +7.2.4 \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index b5b9e206b5..cfde01fce6 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.2.3"); + private static readonly Version Version = new Version("7.2.4"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ad23e7ae24..829178c030 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2547,9 +2547,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7230 + 7240 / - http://localhost:7230 + http://localhost:7240 False False From d2b43457737d78301172dde69e290f44886bd855 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 17:47:33 +1100 Subject: [PATCH 091/249] Fixes: U4-6433 Usercontrol loaded after package installation not executing --- .../src/views/common/legacy.controller.js | 25 ++++++++++++++----- .../developer/Packages/installer.aspx.cs | 4 +-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js index bf6bc287df..8e58535c74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/legacy.controller.js @@ -9,13 +9,26 @@ */ function LegacyController($scope, $routeParams, $element) { - var url = decodeURIComponent($routeParams.url.toLowerCase().trimStart("javascript:")); - var toClean = "*(){}[];:<>\\|'\""; - for (var i = 0; i < toClean.length; i++) { - var reg = new RegExp("\\" + toClean[i], "g"); - url = url.replace(reg, ""); + var url = decodeURIComponent($routeParams.url.toLowerCase().replace(/javascript\:/g, "")); + //split into path and query + var urlParts = url.split("?"); + var extIndex = urlParts[0].lastIndexOf("."); + var ext = extIndex === -1 ? "" : urlParts[0].substr(extIndex); + //path cannot be a js file + if (ext !== ".js" || ext === "") { + //path cannot contain any of these chars + var toClean = "*(){}[];:<>\\|'\""; + for (var i = 0; i < toClean.length; i++) { + var reg = new RegExp("\\" + toClean[i], "g"); + urlParts[0] = urlParts[0].replace(reg, ""); + } + //join cleaned path and query back together + url = urlParts[0] + (urlParts.length === 1 ? "" : ("?" + urlParts[1])); + $scope.legacyPath = url; + } + else { + throw "Invalid url"; } - $scope.legacyPath = url; } angular.module("umbraco").controller('Umbraco.LegacyController', LegacyController); \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs index 351015ddd8..6c75ab0e6e 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs @@ -220,7 +220,7 @@ namespace umbraco.presentation.developer.packages var packageId = 0; int.TryParse(Request.GetItemAsString("pId"), out packageId); - switch (currentStep) + switch (currentStep.ToLowerInvariant()) { case "businesslogic": //first load in the config from the temporary directory @@ -241,7 +241,7 @@ namespace umbraco.presentation.developer.packages Response.Redirect("installer.aspx?installing=refresh&dir=" + dir + "&pId=" + packageId.ToString() + "&customUrl=" + Server.UrlEncode(_installer.Url)); } break; - case "customInstaller": + case "custominstaller": var customControl = Request.GetItemAsString("customControl"); if (customControl.IsNullOrWhiteSpace() == false) From 27e96d6d5b4a10f6ce661398a9ceca3103c82ed8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 18:15:31 +1100 Subject: [PATCH 092/249] Fixes: U4-6436 Localization in the back office doesn't work immediately after app restart --- .../LocalizedTextServiceFileSources.cs | 138 ++++++++++-------- 1 file changed, 75 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index be53d8d16b..934b015079 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -20,12 +20,83 @@ namespace Umbraco.Core.Services //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( private readonly Dictionary _twoLetterCultureConverter = new Dictionary(); + private readonly Lazy>> _xmlSources; + public LocalizedTextServiceFileSources(IRuntimeCacheProvider cache, DirectoryInfo fileSourceFolder) { if (cache == null) throw new ArgumentNullException("cache"); if (fileSourceFolder == null) throw new ArgumentNullException("fileSourceFolder"); + _cache = cache; + //Create the lazy source for the _xmlSources + _xmlSources = new Lazy>>(() => + { + var result = new Dictionary>(); + + if (_fileSourceFolder == null) return result; + + foreach (var fileInfo in _fileSourceFolder.GetFiles("*.xml")) + { + var localCopy = fileInfo; + var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); + + //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct + // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we + // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that + // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) + // that any 4 letter file is named with the actual culture that it is! + CultureInfo culture = null; + if (filename.Length == 2) + { + //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't + //want to load in the entire doc into mem just to read a single value + using (var fs = fileInfo.OpenRead()) + using (var reader = XmlReader.Create(fs)) + { + if (reader.IsStartElement()) + { + if (reader.Name == "language") + { + if (reader.MoveToAttribute("culture")) + { + var cultureVal = reader.Value; + try + { + culture = CultureInfo.GetCultureInfo(cultureVal); + //add to the tracked dictionary + _twoLetterCultureConverter[filename] = culture; + } + catch (CultureNotFoundException) + { + LogHelper.Warn( + string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); + //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise + // an exception will be thrown. + } + } + } + } + } + } + if (culture == null) + { + culture = CultureInfo.GetCultureInfo(filename); + } + + //get the lazy value from cache + result.Add(culture, new Lazy(() => _cache.GetCacheItem( + string.Format("{0}-{1}", typeof(LocalizedTextServiceFileSources).Name, culture.Name), () => + { + using (var fs = localCopy.OpenRead()) + { + return XDocument.Load(fs); + } + }, isSliding: true, timeout: TimeSpan.FromMinutes(10), dependentFiles: new[] { localCopy.FullName }))); + } + return result; + }); + if (fileSourceFolder.Exists == false) { LogHelper.Warn("The folder does not exist: {0}, therefore no sources will be discovered", () => fileSourceFolder.FullName); @@ -42,69 +113,7 @@ namespace Umbraco.Core.Services /// public IDictionary> GetXmlSources() { - var result = new Dictionary>(); - - if (_fileSourceFolder == null) return result; - - foreach (var fileInfo in _fileSourceFolder.GetFiles("*.xml")) - { - var localCopy = fileInfo; - var filename = Path.GetFileNameWithoutExtension(localCopy.FullName).Replace("_", "-"); - - //TODO: Fix this nonsense... would have to wait until v8 to store the language files with their correct - // names instead of storing them as 2 letters but actually having a 4 letter culture. wtf. So now, we - // need to check if the file is 2 letters, then open it to try to find it's 4 letter culture, then use that - // if it's successful. We're going to assume (though it seems assuming in the legacy logic is never a great idea) - // that any 4 letter file is named with the actual culture that it is! - CultureInfo culture = null; - if (filename.Length == 2) - { - //we need to open the file to see if we can read it's 'real' culture, we'll use XmlReader since we don't - //want to load in the entire doc into mem just to read a single value - using (var fs = fileInfo.OpenRead()) - using (var reader = XmlReader.Create(fs)) - { - if (reader.IsStartElement()) - { - if (reader.Name == "language") - { - if (reader.MoveToAttribute("culture")) - { - var cultureVal = reader.Value; - try - { - culture = CultureInfo.GetCultureInfo(cultureVal); - //add to the tracked dictionary - _twoLetterCultureConverter[filename] = culture; - } - catch (CultureNotFoundException) - { - LogHelper.Warn( - string.Format("The culture {0} found in the file {1} is not a valid culture", cultureVal, fileInfo.FullName)); - //If the culture in the file is invalid, we'll just hope the file name is a valid culture below, otherwise - // an exception will be thrown. - } - } - } - } - } - } - if (culture == null) - { - culture = CultureInfo.GetCultureInfo(filename); - } - - //get the lazy value from cache - result.Add(culture, new Lazy(() => _cache.GetCacheItem( - string.Format("{0}-{1}", typeof (LocalizedTextServiceFileSources).Name, culture.Name), () => - { - using (var fs = localCopy.OpenRead()) - { - return XDocument.Load(fs); - } - }, isSliding: true, timeout: TimeSpan.FromMinutes(10), dependentFiles: new[] {localCopy.FullName}))); - } - return result; + return _xmlSources.Value; } //TODO: See other notes in this class, this is purely a hack because we store 2 letter culture file names that contain 4 letter cultures :( @@ -112,6 +121,9 @@ namespace Umbraco.Core.Services { if (twoLetterCulture.Length != 2) Attempt.Fail(); + //This needs to be resolved before continuing so that the _twoLetterCultureConverter cache is initialized + var resolved = _xmlSources.Value; + return _twoLetterCultureConverter.ContainsKey(twoLetterCulture) ? Attempt.Succeed(_twoLetterCultureConverter[twoLetterCulture]) : Attempt.Fail(); From 03b60d4923a0c81fd3f638e04f5781089b319bc1 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 20 Mar 2015 10:16:53 +0100 Subject: [PATCH 093/249] "It's simple until you make it complicated" - So now this logic has been un-complicated: look for the web.config file, if it exists and has a umbracoConfigurationStatus appSetting then don't overwrite the web.config, the user is upgrading umbraco and config transforms can do their thing. Otherwise, copy in a fresh web.config with all the required Umbraco bits. --- build/NuSpecs/tools/install.ps1 | 86 ++++++--------------------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 1816aa8f75..9828d4e89c 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -47,85 +47,31 @@ if ($project) { robocopy $umbracoClientFolder $umbracoClientBackupPath /e /LOG:$copyLogsPath\UmbracoClientBackup.log robocopy $umbracoClientFolderSource $umbracoClientFolder /is /it /e /LOG:$copyLogsPath\UmbracoClientCopy.log } - - $copyWebconfig = $false - # SJ - What can I say: big up for James Newton King for teaching us a hack for detecting if this is a new install vs. an upgrade! - # https://github.com/JamesNK/Newtonsoft.Json/pull/387 - would never have seen this without the controversial pull request.. - Try + $copyWebconfig = $true + $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" + + if(Test-Path $destinationWebConfig) { - # see if user is installing from VS NuGet console - # get reference to the window, the console host and the input history - # copy web.config if "install-package UmbracoCms" was last input - # this is in a try-catch as they might be using the regular NuGet dialog - # instead of package manager console - - $dte2 = Get-Interface $dte ([EnvDTE80.DTE2]) - - $consoleWindow = $(Get-VSComponentModel).GetService([NuGetConsole.IPowerConsoleWindow]) + Try + { + [xml]$config = Get-Content $destinationWebConfig - $props = $consoleWindow.GetType().GetProperties([System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $prop = $props | ? { $_.Name -eq "ActiveHostInfo" } | select -first 1 - - $hostInfo = $prop.GetValue($consoleWindow) - - $history = $hostInfo.WpfConsole.InputHistory.History - - $lastCommand = $history | select -last 1 - - if ($lastCommand) - { - $lastCommand = $lastCommand.Trim().ToLower() - if ($lastCommand.StartsWith("install-package") -and $lastCommand.Contains("umbracocms")) - { - $copyWebconfig = $true + $config.configuration.appSettings.ChildNodes | ForEach-Object { + if($_.key -eq "umbracoConfigurationStatus") + { + # The web.config has an umbraco-specific appSetting in it + # don't overwrite it and let config transforms do their thing + $copyWebconfig = $false + } } - } + } + Catch { } } - Catch { } - - Try - { - # user is installing from VS NuGet dialog - # get reference to the window, then smart output console provider - # copy web.config if messages in buffered console contains "installing...UmbracoCms" in last operation - - $instanceField = [NuGet.Dialog.PackageManagerWindow].GetField("CurrentInstance", [System.Reflection.BindingFlags]::Static -bor [System.Reflection.BindingFlags]::NonPublic) - $consoleField = [NuGet.Dialog.PackageManagerWindow].GetField("_smartOutputConsoleProvider", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $instance = $instanceField.GetValue($null) - - $consoleProvider = $consoleField.GetValue($instance) - - $console = $consoleProvider.CreateOutputConsole($false) - - $messagesField = $console.GetType().GetField("_messages", [System.Reflection.BindingFlags]::Instance -bor [System.Reflection.BindingFlags]::NonPublic) - - $messages = $messagesField.GetValue($console) - - $operations = $messages -split "==============================" - - $lastOperation = $operations | select -last 1 - - if ($lastOperation) - { - $lastOperation = $lastOperation.ToLower() - $lines = $lastOperation -split "`r`n" - $installMatch = $lines | ? { $_.Contains("...umbracocms ") } | select -first 1 - - if ($installMatch) - { - $copyWebconfig = $true - } - } - } - Catch { } if($copyWebconfig -eq $true) { $packageWebConfigSource = Join-Path $rootPath "UmbracoFiles\Web.config" - $destinationWebConfig = Join-Path $projectDestinationPath "Web.config" Copy-Item $packageWebConfigSource $destinationWebConfig -Force } From 907e5e7ca4cb87f45b375268ddabe29aabac5cb6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 20 Mar 2015 11:25:59 +0100 Subject: [PATCH 094/249] Bump version --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index a6175d96d2..ecb394c050 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.2.4 \ No newline at end of file +7.2.5 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 6ef556ffd3..9fd1385311 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.2.4")] -[assembly: AssemblyInformationalVersion("7.2.4")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.2.5")] +[assembly: AssemblyInformationalVersion("7.2.5")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index cfde01fce6..8f9f84d656 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.2.4"); + private static readonly Version Version = new Version("7.2.5"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 829178c030..0e655eabb2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2547,9 +2547,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 7240 + 7250 / - http://localhost:7240 + http://localhost:7250 False False From 53a4d063e03bd4403fc35ce7b6f9f20d26e8e1c6 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Sun, 22 Mar 2015 13:13:51 +0100 Subject: [PATCH 095/249] U4-464 The embedded passwordStrength.js file is GPL-licensed #U4-464 Fixed Due in version 7.2.5 --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 - .../passwordStrength/passwordstrength.js | 148 ------------------ 2 files changed, 149 deletions(-) delete mode 100644 src/Umbraco.Web.UI/umbraco_client/passwordStrength/passwordstrength.js diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0e655eabb2..566ddd5e9c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1878,7 +1878,6 @@ - diff --git a/src/Umbraco.Web.UI/umbraco_client/passwordStrength/passwordstrength.js b/src/Umbraco.Web.UI/umbraco_client/passwordStrength/passwordstrength.js deleted file mode 100644 index a14d529eab..0000000000 --- a/src/Umbraco.Web.UI/umbraco_client/passwordStrength/passwordstrength.js +++ /dev/null @@ -1,148 +0,0 @@ -// -// password_strength_plugin.js -// Copyright (c) 2009 myPocket technologies (www.mypocket-technologies.com) - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// View the GNU General Public License . - -// @author Darren Mason (djmason9@gmail.com) -// @date 1/23/2009 -// @projectDescription Password Strength Meter is a jQuery plug-in provide you smart algorithm to detect a password strength. Based on Firas Kassem orginal plugin - http://phiras.wordpress.com/2007/04/08/password-strength-meter-a-jquery-plugin/ -// @version 1.0.0 -// -// @requires jquery.js (tested with 1.3.1) -// @param shortPass: "shortPass", //optional -// @param badPass: "badPass", //optional -// @param goodPass: "goodPass", //optional -// @param strongPass: "strongPass", //optional -// @param baseStyle: "testresult", //optional -// @param userid: "", //required override -// @param messageloc: 1 //before == 0 or after == 1 -// - - -(function ($) { - $.fn.shortPass = 'The password is too short'; - $.fn.badPass = 'The password is weak'; - $.fn.goodPass = 'Good password'; - $.fn.strongPass = 'Strong password'; - $.fn.samePassword = 'User name and Password are identical.'; - $.fn.resultStyle = ""; - - $.fn.passStrength = function (options) { - - var defaults = { - shortPass: 'shortPass', //optional - badPass: 'badPass', //optional - goodPass: 'goodPass', //optional - strongPass: 'strongPass', //optional - baseStyle: 'testresult', //optional - userid: '', //required override - messageloc: 1, //before == 0 or after == 1 - minLength: 7 - }; - var opts = $.extend(defaults, options); - - return this.each(function () { - var obj = $(this); - - $(obj).unbind().keyup(function () { - - var results = $.fn.teststrength($(this).val(), opts.userid, opts); - var fieldSpan = this.parent; - - if (opts.messageloc === 1) { - $(this).parent().next("." + opts.baseStyle).remove(); - $(this).parent().after(""); - $(this).parent().next("." + opts.baseStyle).addClass($(this).resultStyle).find("strong").text(results); - } - else { - $(this).prev("." + opts.baseStyle).remove(); - $(this).before(""); - $(this).prev("." + opts.baseStyle).addClass($(this).resultStyle).find("strong").text(results); - } - }); - - //FUNCTIONS - $.fn.teststrength = function (password, username, option) { - var score = 0; - - //password < minLength - if (password.length < option.minLength) { this.resultStyle = option.shortPass; return $(this).shortPass; } - - //password == user name - if (password.toLowerCase() == username.toLowerCase()) { this.resultStyle = option.badPass; return $(this).samePassword; } - - //password length - score += password.length // 4; - score += ($.fn.checkRepetition(1, password).length - password.length) // 1; - score += ($.fn.checkRepetition(2, password).length - password.length) // 1; - score += ($.fn.checkRepetition(3, password).length - password.length) // 1; - score += ($.fn.checkRepetition(4, password).length - password.length) // 1; - - //password has 3 numbers - if (password.match(/(.*[0-9].*[0-9].*[0-9])/)) { score += 5; } - - //password has 2 symbols - if (password.match(/(.*[!,@,#,$,%,^,&,*,?,_,~].*[!,@,#,$,%,^,&,*,?,_,~])/)) { score += 5; } - - //password has Upper and Lower chars - if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) { score += 10; } - - //password has number and chars - if (password.match(/([a-zA-Z])/) && password.match(/([0-9])/)) { score += 15; } - // - //password has number and symbol - if (password.match(/([!,@,#,$,%,^,&,*,?,_,~])/) && password.match(/([0-9])/)) { score += 15; } - - //password has char and symbol - if (password.match(/([!,@,#,$,%,^,&,*,?,_,~])/) && password.match(/([a-zA-Z])/)) { score += 15; } - - //password is just a numbers or chars - if (password.match(/^\w+$/) || password.match(/^\d+$/)) { score -= 10; } - - //verifying 0 < score < 100 - if (score < 0) { score = 0; } - if (score > 100) { score = 100; } - - if (score < 34) { this.resultStyle = option.badPass; return $(this).badPass; } - if (score < 68) { this.resultStyle = option.goodPass; return $(this).goodPass; } - - this.resultStyle = option.strongPass; - return $(this).strongPass; - - }; - - }); - }; -})(jQuery); - - -$.fn.checkRepetition = function(pLen, str) { - var res = ''; - for (var i = 0; i < str.length; i++) { - var repeated = true; - - for (var j = 0; j < pLen && (j + i + pLen) < str.length; j++) { - repeated = repeated && (str.charAt(j + i) == str.charAt(j + i + pLen)); - } - if (j < pLen) { repeated = false; } - if (repeated) { - i += pLen - 1; - repeated = false; - } - else { - res += str.charAt(i); - } - } - return res; -}; From e3925f18ef4a3a3ec9b5e7a612b06aa1baa3568e Mon Sep 17 00:00:00 2001 From: Arnoud de Vries Date: Sun, 22 Mar 2015 16:29:53 +0100 Subject: [PATCH 096/249] Fix some spelling mistakes in package installer --- .../umbraco/developer/Packages/installer.aspx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/developer/Packages/installer.aspx b/src/Umbraco.Web.UI/umbraco/developer/Packages/installer.aspx index 63e954dd2d..27ccf1cd63 100644 --- a/src/Umbraco.Web.UI/umbraco/developer/Packages/installer.aspx +++ b/src/Umbraco.Web.UI/umbraco/developer/Packages/installer.aspx @@ -107,7 +107,7 @@

Please note: Installing a package containing several items and files can take some time. Do not refresh the page or navigate away before, the installer - notifies you the install is completed. + notifies you once the install is completed.

@@ -280,9 +280,9 @@

- All items in the package has been installed

+ All items in the package have been installed

- Overview of what was installed can found under "installed package" in the developer + Overview of what was installed can be found under "installed package" in the developer section.

Uninstall is available at the same location.

From c6b393f2e868f411b391d322fdb4006936f8e944 Mon Sep 17 00:00:00 2001 From: Robert Foster Date: Tue, 24 Mar 2015 12:30:28 +1100 Subject: [PATCH 097/249] typo fix in pageIndex Mandate checks --- src/Umbraco.Core/Services/ContentService.cs | 4 ++-- src/Umbraco.Core/Services/MediaService.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 966ae9f0e7..057ca928bd 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -500,7 +500,7 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageSize"); + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) { @@ -530,7 +530,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageSize"); + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = _repositoryFactory.CreateContentRepository(_uowProvider.GetUnitOfWork())) { diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 279ec9f07f..4a42439024 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -408,7 +408,7 @@ namespace Umbraco.Core.Services public IEnumerable GetPagedChildren(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy, Direction orderDirection, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageSize"); + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = _repositoryFactory.CreateMediaRepository(_uowProvider.GetUnitOfWork())) { @@ -437,7 +437,7 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") { - Mandate.ParameterCondition(pageIndex >= 0, "pageSize"); + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); Mandate.ParameterCondition(pageSize > 0, "pageSize"); using (var repository = _repositoryFactory.CreateMediaRepository(_uowProvider.GetUnitOfWork())) { From 4e195543eae2ef118ab2c9190a34e4ba4a6360e0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 15:12:58 +1100 Subject: [PATCH 098/249] fixes null ref with component renderer in UmbracoHelper --- src/Umbraco.Web/UmbracoHelper.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 0429cf83ae..383897b2f2 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -245,7 +245,7 @@ namespace Umbraco.Web /// public IHtmlString RenderTemplate(int pageId, int? altTemplateId = null) { - return _componentRenderer.RenderTemplate(pageId, altTemplateId); + return UmbracoComponentRenderer.RenderTemplate(pageId, altTemplateId); } #region RenderMacro @@ -257,7 +257,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias) { - return _componentRenderer.RenderMacro(alias, new { }); + return UmbracoComponentRenderer.RenderMacro(alias, new { }); } /// @@ -268,7 +268,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias, object parameters) { - return _componentRenderer.RenderMacro(alias, parameters.ToDictionary()); + return UmbracoComponentRenderer.RenderMacro(alias, parameters.ToDictionary()); } /// @@ -279,7 +279,7 @@ namespace Umbraco.Web /// public IHtmlString RenderMacro(string alias, IDictionary parameters) { - return _componentRenderer.RenderMacro(alias, parameters); + return UmbracoComponentRenderer.RenderMacro(alias, parameters); } #endregion From ffaf1f96b9484dd74b2181c60fc71e934ae480f7 Mon Sep 17 00:00:00 2001 From: Per Osbeck Date: Tue, 24 Mar 2015 09:28:53 +0100 Subject: [PATCH 099/249] Fix for U4-6444 --- .../src/common/services/mediahelper.service.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 67dbcecc54..68ca32efbc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -327,6 +327,11 @@ function mediaHelper(umbRequestHelper) { * @param {string} imagePath Image path, ex: /media/1234/my-image.jpg */ detectIfImageByExtension: function (imagePath) { + + if (!imagePath) { + return false; + } + var lowered = imagePath.toLowerCase(); var ext = lowered.substr(lowered.lastIndexOf(".") + 1); return ("," + Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes + ",").indexOf("," + ext + ",") !== -1; From 9dfccd86fd3961712be529042941c2a62f7c6bb8 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 24 Mar 2015 20:09:48 +0100 Subject: [PATCH 100/249] U4-2499 Remove the possibility for accessing pages with nodeid's or give a redirect option #U4-2499 Fixed Due in version 7.2.5 --- .../UmbracoSettings/IWebRoutingSection.cs | 2 ++ .../Configuration/UmbracoSettings/WebRoutingElement.cs | 5 +++++ .../config/umbracoSettings.Release.config | 6 +++++- src/Umbraco.Web.UI/config/umbracoSettings.config | 10 +++++++--- src/Umbraco.Web/Routing/ContentFinderByIdPath.cs | 4 ++++ 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs index 393387ecfa..f3d42b6904 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IWebRoutingSection.cs @@ -8,6 +8,8 @@ bool DisableAlternativeTemplates { get; } + bool DisableFindContentByIdPath { get; } + string UrlProviderMode { get; } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs index 82c5a37575..f5b71eb2c7 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/WebRoutingElement.cs @@ -21,6 +21,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return (bool) base["disableAlternativeTemplates"]; } } + [ConfigurationProperty("disableFindContentByIdPath", DefaultValue = "false")] + public bool DisableFindContentByIdPath + { + get { return (bool) base["disableFindContentByIdPath"]; } + } [ConfigurationProperty("urlProviderMode", DefaultValue = "AutoLegacy")] public string UrlProviderMode diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index 2959b0388b..b371f536e6 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -137,10 +137,14 @@ will make Umbraco render the content on the current page with the template you requested, for example: http://mysite.com/about-us/?altTemplate=Home and http://mysite.com/about-us/Home would render the "About Us" page with a template with the alias Home. Setting this setting to true stops that behavior + @disableFindContentByIdPath + By default you can call any content Id in the url and show the content with that id, for example: + http://mysite.com/1092 or http://mysite.com/1092.aspx would render the content with id 1092. Setting + this setting to true stops that behavior --> + internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false" disableFindContentByIdPath="false"> diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index e13441b802..f03cc08d9f 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -295,10 +295,14 @@ will make Umbraco render the content on the current page with the template you requested, for example: http://mysite.com/about-us/?altTemplate=Home and http://mysite.com/about-us/Home would render the "About Us" page with a template with the alias Home. Setting this setting to true stops that behavior + @disableFindContentByIdPath + By default you can call any content Id in the url and show the content with that id, for example: + http://mysite.com/1092 or http://mysite.com/1092.aspx would render the content with id 1092. Setting + this setting to true stops that behavior --> - + \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index 9bae24c5dd..90da48039c 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs @@ -2,6 +2,7 @@ using System; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core; +using Umbraco.Core.Configuration; namespace Umbraco.Web.Routing { @@ -20,6 +21,9 @@ namespace Umbraco.Web.Routing /// A value indicating whether an Umbraco document was found and assigned. public bool TryFindContent(PublishedContentRequest docRequest) { + if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableFindContentByIdPath) + return false; + IPublishedContent node = null; var path = docRequest.Uri.GetAbsolutePathDecoded(); From e7754710136515643fc881b9c816e64ae5dcd32f Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 25 Mar 2015 17:08:53 +0100 Subject: [PATCH 101/249] Set disableFindContentByIdPath=false for local debugging of Umb source (the release config was already correct) --- src/Umbraco.Web.UI/config/umbracoSettings.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index f03cc08d9f..616b5ba424 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -302,7 +302,7 @@ --> + internalRedirectPreservesTemplate="false" disableAlternativeTemplates="false" disableFindContentByIdPath="false"> \ No newline at end of file From 6419f301f76db032a5a7af3f94085435274514a0 Mon Sep 17 00:00:00 2001 From: kgiszewski Date: Wed, 25 Mar 2015 16:30:47 -0400 Subject: [PATCH 102/249] Fix for U4-6462 --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index e58bc75d70..c5c6148b2f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1010,7 +1010,7 @@ To manage your website, simply open the Umbraco back office and start adding con Editor Excerpt field Language - Login + Username Start Node in Media Library Sections Disable Umbraco Access @@ -1029,7 +1029,7 @@ To manage your website, simply open the Umbraco back office and start adding con Select pages to modify their permissions Search all children Start Node in Content - Username + Name User permissions User type User types From 0c107fd8699577ef1e026fb166311742f6c545e6 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 25 Mar 2015 21:52:39 +0100 Subject: [PATCH 103/249] Ensures footer breadcrumb in content and media returned in correct order --- .../src/views/content/content.edit.controller.js | 2 +- .../src/views/media/media.edit.controller.js | 2 +- src/Umbraco.Web/Editors/EntityController.cs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index 4f2a259779..03de8af21e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -37,7 +37,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $ if (content.parentId && content.parentId != -1) { entityResource.getAncestors(content.id, "document") .then(function (anc) { - $scope.ancestors = anc.reverse(); + $scope.ancestors = anc; }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 08b269077a..b74cfeb463 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -71,7 +71,7 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, enti //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors($routeParams.id, "media") .then(function (anc) { - $scope.ancestors = anc.reverse(); + $scope.ancestors = anc; }); } diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index b12d19d032..4c1b8dd849 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -557,6 +557,7 @@ namespace Umbraco.Web.Editors return Services.EntityService.GetAll(objectType.Value, ids) .WhereNotNull() + .OrderBy(x => x.Level) .Select(Mapper.Map); } //now we need to convert the unknown ones From e001ed93c1ae7cc78c7e9534e6b17c0796ac86df Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 10:16:49 +1100 Subject: [PATCH 104/249] Updates BatchedDatabaseServerMessenger to have an abstract GetBatch method instead of having a delegate in the ctor --- .../Sync/BatchedDatabaseServerMessenger.cs | 13 +++++-------- src/Umbraco.Web/BatchedDatabaseServerMessenger.cs | 8 ++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs index 5c439377f3..2928b55919 100644 --- a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs @@ -14,21 +14,18 @@ namespace Umbraco.Core.Sync // public abstract class BatchedDatabaseServerMessenger : DatabaseServerMessenger { - private readonly Func> _getBatch; - protected BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options, - Func> getBatch) + protected BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options) : base(appContext, enableDistCalls, options) { - if (getBatch == null) - throw new ArgumentNullException("getBatch"); - _getBatch = getBatch; } + protected abstract ICollection GetBatch(bool ensureHttpContext); + public void FlushBatch() { - var batch = _getBatch(false); + var batch = GetBatch(false); if (batch == null) return; var instructions = batch.SelectMany(x => x.Instructions).ToArray(); @@ -64,7 +61,7 @@ namespace Umbraco.Core.Sync Type idType = null, string json = null) { - var batch = _getBatch(true); + var batch = GetBatch(true); if (batch == null) throw new Exception("Failed to get a batch."); diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 4bf4852f77..0a943f24f9 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web public class BatchedDatabaseServerMessenger : Core.Sync.BatchedDatabaseServerMessenger { public BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options) - : base(appContext, enableDistCalls, options, GetBatch) + : base(appContext, enableDistCalls, options) { UmbracoApplicationBase.ApplicationStarted += Application_Started; UmbracoModule.EndRequest += UmbracoModule_EndRequest; @@ -60,12 +60,12 @@ namespace Umbraco.Web FlushBatch(); } - private static ICollection GetBatch(bool ensure) + protected override ICollection GetBatch(bool ensureHttpContext) { var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; if (httpContext == null) { - if (ensure) + if (ensureHttpContext) throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); return null; } @@ -74,7 +74,7 @@ namespace Umbraco.Web // no thread-safety here because it'll run in only 1 thread (request) at a time var batch = (ICollection)httpContext.Items[key]; - if (batch == null && ensure) + if (batch == null && ensureHttpContext) httpContext.Items[key] = batch = new List(); return batch; } From c73749ea545b8aee14224b1def153f50216aa6dc Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 10:25:25 +1100 Subject: [PATCH 105/249] Removes ctor's accepting delegates and instead makes them abstract methods, adds some docs --- .../Sync/BatchedDatabaseServerMessenger.cs | 14 +++--- .../Sync/BatchedWebServiceServerMessenger.cs | 44 +++++++------------ .../Sync/WebServiceServerMessenger.cs | 9 ++-- .../BatchedDatabaseServerMessenger.cs | 6 +++ .../BatchedWebServiceServerMessenger.cs | 20 ++++++--- 5 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs index 2928b55919..9eaf557170 100644 --- a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs @@ -7,11 +7,15 @@ using umbraco.interfaces; namespace Umbraco.Core.Sync { - // abstract because it needs to be inherited by a class that will - // - trigger FlushBatch() when appropriate - // - trigger Boot() when appropriate - // - trigger Sync() when appropriate - // + /// + /// An that works by storing messages in the database. + /// + /// + /// abstract because it needs to be inherited by a class that will + /// - trigger FlushBatch() when appropriate + /// - trigger Boot() when appropriate + /// - trigger Sync() when appropriate + /// public abstract class BatchedDatabaseServerMessenger : DatabaseServerMessenger { diff --git a/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs index 57dd273f2a..9ab6d08455 100644 --- a/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs +++ b/src/Umbraco.Core/Sync/BatchedWebServiceServerMessenger.cs @@ -5,52 +5,40 @@ using umbraco.interfaces; namespace Umbraco.Core.Sync { - // abstract because it needs to be inherited by a class that will - // - implement ProcessBatch() - // - trigger FlushBatch() when appropriate - // + /// + /// An that works by messaging servers via web services. + /// + /// + /// Abstract because it needs to be inherited by a class that will + /// - implement ProcessBatch() + /// - trigger FlushBatch() when appropriate + /// internal abstract class BatchedWebServiceServerMessenger : WebServiceServerMessenger { - private readonly Func> _getBatch; - - internal BatchedWebServiceServerMessenger(Func> getBatch) + internal BatchedWebServiceServerMessenger() { - if (getBatch == null) - throw new ArgumentNullException("getBatch"); - - _getBatch = getBatch; } - internal BatchedWebServiceServerMessenger(string login, string password, Func> getBatch) + internal BatchedWebServiceServerMessenger(string login, string password) : base(login, password) { - if (getBatch == null) - throw new ArgumentNullException("getBatch"); - - _getBatch = getBatch; } - internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls, Func> getBatch) + internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls) : base(login, password, useDistributedCalls) { - if (getBatch == null) - throw new ArgumentNullException("getBatch"); - - _getBatch = getBatch; } - protected BatchedWebServiceServerMessenger(Func> getLoginAndPassword, Func> getBatch) + protected BatchedWebServiceServerMessenger(Func> getLoginAndPassword) : base(getLoginAndPassword) { - if (getBatch == null) - throw new ArgumentNullException("getBatch"); - - _getBatch = getBatch; } + protected abstract ICollection GetBatch(bool ensureHttpContext); + protected void FlushBatch() { - var batch = _getBatch(false); + var batch = GetBatch(false); if (batch == null) return; var batcha = batch.ToArray(); @@ -82,7 +70,7 @@ namespace Umbraco.Core.Sync Type idType = null, string json = null) { - var batch = _getBatch(true); + var batch = GetBatch(true); if (batch == null) throw new Exception("Failed to get a batch."); diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs index 1c63b591b7..e5717fbfdc 100644 --- a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs +++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs @@ -14,10 +14,11 @@ namespace Umbraco.Core.Sync /// /// An that works by messaging servers via web services. /// - // - // this messenger sends ALL instructions to ALL servers, including the local server. - // the CacheRefresher web service will run ALL instructions, so there may be duplicated, - // except for "bulk" refresh, where it excludes those coming from the local server + /// + /// this messenger sends ALL instructions to ALL servers, including the local server. + /// the CacheRefresher web service will run ALL instructions, so there may be duplicated, + /// except for "bulk" refresh, where it excludes those coming from the local server + /// // // TODO see Message() method: stop sending to local server! // just need to figure out WebServerUtility permissions issues, if any diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 0a943f24f9..38de7ef3cc 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -8,6 +8,12 @@ using Umbraco.Web.Routing; namespace Umbraco.Web { + /// + /// An implementation that works by storing messages in the database. + /// + /// + /// This binds to appropriate umbraco events in order to trigger the Boot(), Sync() & FlushBatch() calls + /// public class BatchedDatabaseServerMessenger : Core.Sync.BatchedDatabaseServerMessenger { public BatchedDatabaseServerMessenger(ApplicationContext appContext, bool enableDistCalls, DatabaseServerMessengerOptions options) diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs index fca0b193db..5fc3b48eee 100644 --- a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -4,38 +4,44 @@ using Umbraco.Core.Sync; namespace Umbraco.Web { + /// + /// An that works by messaging servers via web services. + /// + /// + /// This binds to appropriate umbraco events in order to trigger the FlushBatch() calls + /// internal class BatchedWebServiceServerMessenger : Core.Sync.BatchedWebServiceServerMessenger { internal BatchedWebServiceServerMessenger() - : base(GetBatch) + : base() { UmbracoModule.EndRequest += UmbracoModule_EndRequest; } internal BatchedWebServiceServerMessenger(string login, string password) - : base(login, password, GetBatch) + : base(login, password) { UmbracoModule.EndRequest += UmbracoModule_EndRequest; } internal BatchedWebServiceServerMessenger(string login, string password, bool useDistributedCalls) - : base(login, password, useDistributedCalls, GetBatch) + : base(login, password, useDistributedCalls) { UmbracoModule.EndRequest += UmbracoModule_EndRequest; } public BatchedWebServiceServerMessenger(Func> getLoginAndPassword) - : base(getLoginAndPassword, GetBatch) + : base(getLoginAndPassword) { UmbracoModule.EndRequest += UmbracoModule_EndRequest; } - private static ICollection GetBatch(bool ensure) + protected override ICollection GetBatch(bool ensureHttpContext) { var httpContext = UmbracoContext.Current == null ? null : UmbracoContext.Current.HttpContext; if (httpContext == null) { - if (ensure) + if (ensureHttpContext) throw new NotSupportedException("Cannot execute without a valid/current UmbracoContext with an HttpContext assigned."); return null; } @@ -44,7 +50,7 @@ namespace Umbraco.Web // no thread-safety here because it'll run in only 1 thread (request) at a time var batch = (ICollection)httpContext.Items[key]; - if (batch == null && ensure) + if (batch == null && ensureHttpContext) httpContext.Items[key] = batch = new List(); return batch; } From a9b71f853379b32980382db304eb67d377fb6589 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 12:37:03 +1100 Subject: [PATCH 106/249] adds docs --- .../Sync/RefreshInstructionEnvelope.cs | 6 ++++- src/Umbraco.Core/Sync/ServerMessengerBase.cs | 23 ++++++++++++++++++- src/Umbraco.Web/Cache/DistributedCache.cs | 9 ++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs index 12922d6dab..2a198ef99c 100644 --- a/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs +++ b/src/Umbraco.Core/Sync/RefreshInstructionEnvelope.cs @@ -3,7 +3,11 @@ using umbraco.interfaces; namespace Umbraco.Core.Sync { - public class RefreshInstructionEnvelope + /// + /// Used for any 'Batched' instances which specifies a set of targeting a collection of + /// + /// + public sealed class RefreshInstructionEnvelope { public RefreshInstructionEnvelope(IEnumerable servers, ICacheRefresher refresher, IEnumerable instructions) { diff --git a/src/Umbraco.Core/Sync/ServerMessengerBase.cs b/src/Umbraco.Core/Sync/ServerMessengerBase.cs index 2585922078..5da0837c2d 100644 --- a/src/Umbraco.Core/Sync/ServerMessengerBase.cs +++ b/src/Umbraco.Core/Sync/ServerMessengerBase.cs @@ -144,6 +144,16 @@ namespace Umbraco.Core.Sync #region Deliver + /// + /// Executes the non strongly typed on the local/current server + /// + /// + /// + /// + /// + /// + /// Since this is only for non strongly typed it will throw for message types that by instance + /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, IEnumerable ids = null, string json = null) { if (refresher == null) throw new ArgumentNullException("refresher"); @@ -195,7 +205,18 @@ namespace Umbraco.Core.Sync throw new NotSupportedException("Invalid message type " + messageType); } } - + + /// + /// Executes the strongly typed on the local/current server + /// + /// + /// + /// + /// + /// + /// + /// Since this is only for strongly typed it will throw for message types that are not by instance + /// protected void DeliverLocal(ICacheRefresher refresher, MessageType messageType, Func getId, IEnumerable instances) { if (refresher == null) throw new ArgumentNullException("refresher"); diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index db67600757..eb46f91ad1 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -10,8 +10,13 @@ namespace Umbraco.Web.Cache /// Represents the entry point into Umbraco's distributed cache infrastructure. /// /// - /// The distributed cache infrastructure ensures that distributed caches are - /// invalidated properly in load balancing environments. + /// + /// The distributed cache infrastructure ensures that distributed caches are + /// invalidated properly in load balancing environments. + /// + /// + /// Distribute caches include static (in-memory) cache, runtime cache, front-end content cache, Examine/Lucene indexes + /// /// public sealed class DistributedCache { From 12fd5c642d3b78a36194ae1a2d31b639e47d0112 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 12:47:48 +1100 Subject: [PATCH 107/249] Fixes a copy local and specific version ref for a webapi assembly --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + src/Umbraco.Web/Umbraco.Web.csproj | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cc91eac23e..f44b906b93 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -209,6 +209,7 @@ ..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll + False True diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 4bb3b41651..9c9405accd 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -176,6 +176,8 @@ ..\packages\Microsoft.AspNet.WebApi.Client.4.0.30506.0\lib\net40\System.Net.Http.Formatting.dll + False + True ..\packages\Microsoft.Net.Http.2.2.28\lib\net45\System.Net.Http.Primitives.dll From ef9cc2da3a3dd66acb56c8fc0e894ce5bc050193 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 12:58:04 +1100 Subject: [PATCH 108/249] removes WebServerUtility since that original code was only for azure website dist cache when using web services and server registrars (POC) so we also don't need the extra nuget ref either. --- src/Umbraco.Web/Umbraco.Web.csproj | 5 -- src/Umbraco.Web/WebServerUtility.cs | 79 ----------------------------- src/Umbraco.Web/packages.config | 1 - 3 files changed, 85 deletions(-) delete mode 100644 src/Umbraco.Web/WebServerUtility.cs diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9c9405accd..1570bbfb2f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -132,10 +132,6 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - ..\packages\Microsoft.Web.Administration.7.0.0.0\lib\net20\Microsoft.Web.Administration.dll - True - True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -1892,7 +1888,6 @@ True Reference.map - diff --git a/src/Umbraco.Web/WebServerUtility.cs b/src/Umbraco.Web/WebServerUtility.cs deleted file mode 100644 index 7ea9e8ea25..0000000000 --- a/src/Umbraco.Web/WebServerUtility.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Hosting; -using Umbraco.Core; -using Microsoft.Web.Administration; - -namespace Umbraco.Web -{ - internal class WebServerUtility - { - // NOTE - // - // there's some confusion with Microsoft.Web.Administration versions - // 7.0.0.0 is installed by NuGet and will read IIS settings - // 7.9.0.0 comes with IIS Express and will read IIS Express - // we want to use 7.0.0.0 when building - // and then... there are further versions that are N/A on NuGet - // - // Umbraco uses 7.0.0.0 from NuGet - // IMPORTANT: and then, the reference's SpecificVersion property MUST be set to true - // otherwise we might build with 7.9.0.0 and end up in troubles (reading IIS Express - // instead of IIS even when running IIS) - IIS Express has a binding redirect from - // 7.0.0.0 to 7.9.0.0 so it's fine. - // - // read: - // http://stackoverflow.com/questions/11208270/microsoft-web-administration-servermanager-looking-in-wrong-directory-for-iisexp - // http://stackoverflow.com/questions/8467908/how-to-use-servermanager-to-read-iis-sites-not-iis-express-from-class-library - // http://stackoverflow.com/questions/25812169/microsoft-web-administration-servermanager-is-connecting-to-the-iis-express-inst - - public static IEnumerable GetBindings() - { - // FIXME - // which of these methods shall we use? - // what about permissions, trust, etc? - - //return GetBindings2(); - throw new NotImplementedException(); - } - - private static IEnumerable GetBindings1() - { - // get the site name - var siteName = HostingEnvironment.SiteName; - - // get the site from the sites section from the AppPool.config - var sitesSection = WebConfigurationManager.GetSection(null, null, "system.applicationHost/sites"); - var site = sitesSection.GetCollection().FirstOrDefault(x => ((string) x["name"]).InvariantEquals(siteName)); - if (site == null) - return Enumerable.Empty(); - - return site.GetCollection("bindings") - .Where(x => ((string) x["protocol"]).StartsWith("http", StringComparison.OrdinalIgnoreCase)) - .Select(x => - { - var bindingInfo = (string) x["bindingInformation"]; - var parts = bindingInfo.Split(':'); // can count be != 3 ?? - return new Uri(x["protocol"] + "://" + parts[2] + ":" + parts[1] + "/"); - }); - } - - private static IEnumerable GetBindings2() - { - // get the site name - var siteName = HostingEnvironment.SiteName; - - // get the site from the server manager - var mgr = new ServerManager(); - var site = mgr.Sites.FirstOrDefault(x => x.Name.InvariantEquals(siteName)); - if (site == null) - return Enumerable.Empty(); - - // get the bindings - return site.Bindings - .Where(x => x.Protocol.StartsWith("http", StringComparison.OrdinalIgnoreCase)) - .Select(x => new Uri(x.Protocol + "://" + x.Host + ":" + x.EndPoint.Port + "/")); - } - } -} diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index f624d5b6d5..b08c9193ee 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -17,7 +17,6 @@ - From a82035061ccbbc1435d00289becafafe7811c19d Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 29 Jan 2015 12:45:44 +1100 Subject: [PATCH 109/249] Updates BackgroundTaskRunner to support async operations. Updates the content xml cache file persisting to use a IBackgroundTask instead of the strange queuing for persistence - which was super strange because in many cases another request thread will actually be the thread that is persisting the xml file than the request thread that requested it created. This implementation is far better, the xml file will be persisted on a non request thread and will handle multiple documents being published at the same time guaranteeing that the latest published version is the one persisted. The file persistence is also web aware (due to how BackgroundTaskRunner works) so during app shutdown the file will still be written if it's currently being processed. --- src/Umbraco.Core/XmlExtensions.cs | 28 ++++ .../Scheduling/BackgroundTaskRunnerTests.cs | 10 ++ .../XmlCacheFilePersister.cs | 104 ++++++++++++ .../Scheduling/BackgroundTaskRunner.cs | 32 ++-- src/Umbraco.Web/Scheduling/IBackgroundTask.cs | 3 + src/Umbraco.Web/Scheduling/LogScrubber.cs | 11 ++ .../Scheduling/ScheduledPublishing.cs | 11 ++ src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 11 ++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/UmbracoModule.cs | 28 +--- .../umbraco.presentation/content.cs | 149 +++--------------- .../umbraco.presentation/requestModule.cs | 8 +- 12 files changed, 227 insertions(+), 169 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs diff --git a/src/Umbraco.Core/XmlExtensions.cs b/src/Umbraco.Core/XmlExtensions.cs index 02ebc07490..8784cbfb30 100644 --- a/src/Umbraco.Core/XmlExtensions.cs +++ b/src/Umbraco.Core/XmlExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text; +using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; @@ -13,6 +16,31 @@ namespace Umbraco.Core /// internal static class XmlExtensions { + /// + /// Saves the xml document async + /// + /// + /// + /// + public static async Task SaveAsync(this XmlDocument xdoc, string filename) + { + if (xdoc.DocumentElement == null) + throw new XmlException("Cannot save xml document, there is no root element"); + + using (var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize: 4096, useAsync: true)) + using (var xmlWriter = XmlWriter.Create(fs, new XmlWriterSettings + { + Async = true, + Encoding = Encoding.UTF8, + Indent = true + })) + { + //NOTE: There are no nice methods to write it async, only flushing it async. We + // could implement this ourselves but it'd be a very manual process. + xdoc.WriteTo(xmlWriter); + await xmlWriter.FlushAsync(); + } + } public static bool HasAttribute(this XmlAttributeCollection attributes, string attributeName) { diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 8b1981bc9e..7cc763e534 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -265,6 +265,16 @@ namespace Umbraco.Tests.Scheduling public Guid UniqueId { get; protected set; } public abstract void Run(); + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } + public abstract void Cancel(); public DateTime Queued { get; set; } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs new file mode 100644 index 0000000000..482a666538 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -0,0 +1,104 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.PublishedCache.XmlPublishedCache +{ + /// + /// This is the background task runner that persists the xml file to the file system + /// + /// + /// This is used so that all file saving is done on a web aware worker background thread and all logic is performed async so this + /// process will not interfere with any web requests threads. This is also done as to not require any global locks and to ensure that + /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting + /// xml structure since the file writes are queued. + /// + internal class XmlCacheFilePersister : DisposableObject, IBackgroundTask + { + private readonly XmlDocument _xDoc; + private readonly string _xmlFileName; + private readonly ProfilingLogger _logger; + + public XmlCacheFilePersister(XmlDocument xDoc, string xmlFileName, ProfilingLogger logger) + { + _xDoc = xDoc; + _xmlFileName = xmlFileName; + _logger = logger; + } + + public async Task RunAsync() + { + await PersistXmlToFileAsync(_xDoc); + } + + public bool IsAsync + { + get { return true; } + } + + /// + /// Persist a XmlDocument to the Disk Cache + /// + /// + internal async Task PersistXmlToFileAsync(XmlDocument xmlDoc) + { + if (xmlDoc != null) + { + using (_logger.DebugDuration( + string.Format("Saving content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread), + string.Format("Saved content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread))) + { + try + { + // Try to create directory for cache path if it doesn't yet exist + var directoryName = Path.GetDirectoryName(_xmlFileName); + // create dir if it is not there, if it's there, this will proceed as normal + Directory.CreateDirectory(directoryName); + + await xmlDoc.SaveAsync(_xmlFileName); + } + catch (Exception ee) + { + // If for whatever reason something goes wrong here, invalidate disk cache + DeleteXmlCache(); + + LogHelper.Error("Error saving content to disk", ee); + } + } + + + } + } + + private void DeleteXmlCache() + { + if (File.Exists(_xmlFileName) == false) return; + + // Reset file attributes, to make sure we can delete file + try + { + File.SetAttributes(_xmlFileName, FileAttributes.Normal); + } + finally + { + File.Delete(_xmlFileName); + } + } + + protected override void DisposeResources() + { + } + + public void Run() + { + throw new NotImplementedException(); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index a0c6720a88..60eef43b17 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -146,7 +146,8 @@ namespace Umbraco.Web.Scheduling T remainingTask; while (_tasks.TryTake(out remainingTask)) { - ConsumeTaskInternal(remainingTask); + ConsumeTaskInternalAsync(remainingTask) + .Wait(); //block until it completes } } @@ -178,7 +179,7 @@ namespace Umbraco.Web.Scheduling var token = _tokenSource.Token; _consumer = Task.Factory.StartNew(() => - StartThread(token), + StartThreadAsync(token), token, _dedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, TaskScheduler.Default); @@ -197,7 +198,7 @@ namespace Umbraco.Web.Scheduling /// Invokes a new worker thread to consume tasks /// /// - private void StartThread(CancellationToken token) + private async Task StartThreadAsync(CancellationToken token) { // Was cancellation already requested? if (token.IsCancellationRequested) @@ -206,14 +207,14 @@ namespace Umbraco.Web.Scheduling token.ThrowIfCancellationRequested(); } - TakeAndConsumeTask(token); + await TakeAndConsumeTaskAsync(token); } /// /// Trys to get a task from the queue, if there isn't one it will wait a second and try again /// /// - private void TakeAndConsumeTask(CancellationToken token) + private async Task TakeAndConsumeTaskAsync(CancellationToken token) { if (token.IsCancellationRequested) { @@ -235,25 +236,25 @@ namespace Umbraco.Web.Scheduling // cancel when we shutdown foreach (var t in _tasks.GetConsumingEnumerable(token)) { - ConsumeTaskCancellable(t, token); + await ConsumeTaskCancellableAsync(t, token); } //recurse and keep going - TakeAndConsumeTask(token); + await TakeAndConsumeTaskAsync(token); } else { T repositoryTask; while (_tasks.TryTake(out repositoryTask)) { - ConsumeTaskCancellable(repositoryTask, token); + await ConsumeTaskCancellableAsync(repositoryTask, token); } //the task will end here } } - internal void ConsumeTaskCancellable(T task, CancellationToken token) + internal async Task ConsumeTaskCancellableAsync(T task, CancellationToken token) { if (token.IsCancellationRequested) { @@ -266,10 +267,10 @@ namespace Umbraco.Web.Scheduling token.ThrowIfCancellationRequested(); } - ConsumeTaskInternal(task); + await ConsumeTaskInternalAsync(task); } - private void ConsumeTaskInternal(T task) + private async Task ConsumeTaskInternalAsync(T task) { try { @@ -279,7 +280,14 @@ namespace Umbraco.Web.Scheduling { using (task) { - task.Run(); + if (task.IsAsync) + { + await task.RunAsync(); + } + else + { + task.Run(); + } } } catch (Exception e) diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs index 343f076b2a..48522aeb5f 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -1,9 +1,12 @@ using System; +using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { internal interface IBackgroundTask : IDisposable { void Run(); + Task RunAsync(); + bool IsAsync { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 1c7a8f3537..2edbe80726 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using System.Web; using System.Web.Caching; using umbraco.BusinessLogic; @@ -49,5 +50,15 @@ namespace Umbraco.Web.Scheduling Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); } } + + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 8a64098764..cacb4e133d 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; using System.Net; using System.Text; +using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; @@ -75,5 +76,15 @@ namespace Umbraco.Web.Scheduling } } } + + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 94c035631f..ddcb9ea533 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Linq; using System.Net; +using System.Threading.Tasks; using System.Xml; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -106,5 +107,15 @@ namespace Umbraco.Web.Scheduling } } } + + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 82433abf49..8cd6f55046 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -543,6 +543,7 @@ ASPXCodeBehind + True True diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 633df5c6d5..f2d216f2fd 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Security.Principal; using System.Threading; +using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.Routing; @@ -529,24 +530,8 @@ namespace Umbraco.Web urlRouting.PostResolveRequestCache(context); } - /// - /// Checks if the xml cache file needs to be updated/persisted - /// - /// - /// - /// TODO: This needs an overhaul, see the error report created here: - /// https://docs.google.com/document/d/1neGE3q3grB4lVJfgID1keWY2v9JYqf-pw75sxUUJiyo/edit - /// - static void PersistXmlCache(HttpContextBase httpContext) - { - if (content.Instance.IsXmlQueuedForPersistenceToFile) - { - content.Instance.RemoveXmlFilePersistenceQueue(); - content.Instance.PersistXmlToFile(); - } - } - - /// + + /// /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request /// /// @@ -613,13 +598,6 @@ namespace Umbraco.Web ProcessRequest(new HttpContextWrapper(httpContext)); }; - // used to check if the xml cache file needs to be updated/persisted - app.PostRequestHandlerExecute += (sender, e) => - { - var httpContext = ((HttpApplication)sender).Context; - PersistXmlCache(new HttpContextWrapper(httpContext)); - }; - app.EndRequest += (sender, args) => { var httpContext = ((HttpApplication)sender).Context; diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 30bfd1c901..2905abaf99 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -1,33 +1,28 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; -using System.Linq; using System.Threading; +using System.Threading.Tasks; using System.Web; using System.Xml; -using System.Xml.XPath; -using umbraco.cms.presentation; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using umbraco.BusinessLogic; -using umbraco.BusinessLogic.Actions; -using umbraco.BusinessLogic.Utils; using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.cache; using umbraco.cms.businesslogic.web; using Umbraco.Core.Models; +using Umbraco.Core.Profiling; using umbraco.DataLayer; using umbraco.presentation.nodeFactory; using Umbraco.Web; -using Action = umbraco.BusinessLogic.Actions.Action; +using Umbraco.Web.PublishedCache.XmlPublishedCache; +using Umbraco.Web.Scheduling; using Node = umbraco.NodeFactory.Node; -using Umbraco.Core; using File = System.IO.File; namespace umbraco @@ -37,6 +32,8 @@ namespace umbraco /// public class content { + private static readonly BackgroundTaskRunner FilePersister = new BackgroundTaskRunner(dedicatedThread: true); + #region Declarations // Sync access to disk file @@ -75,7 +72,6 @@ namespace umbraco #endregion - #region Singleton private static readonly Lazy LazyInstance = new Lazy(() => new content()); @@ -320,8 +316,8 @@ namespace umbraco // and clear the queue in case is this a web request, we don't want it reprocessing. if (UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache) { - RemoveXmlFilePersistenceQueue(); - PersistXmlToFile(xmlDoc); + FilePersister.Add(new XmlCacheFilePersister(xmlDoc, UmbracoXmlDiskCacheFileName , + new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); } } } @@ -929,54 +925,6 @@ namespace umbraco #region Protected & Private methods - internal const string PersistenceFlagContextKey = "vnc38ykjnkjdnk2jt98ygkxjng"; - - /// - /// Removes the flag that queues the file for persistence - /// - internal void RemoveXmlFilePersistenceQueue() - { - if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null) - { - UmbracoContext.Current.HttpContext.Application.Lock(); - UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] = null; - UmbracoContext.Current.HttpContext.Application.UnLock(); - } - } - - internal bool IsXmlQueuedForPersistenceToFile - { - get - { - if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null) - { - bool val = UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] != null; - if (val) - { - DateTime persistenceTime = DateTime.MinValue; - try - { - persistenceTime = (DateTime)UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey]; - if (persistenceTime > GetCacheFileUpdateTime()) - { - return true; - } - else - { - RemoveXmlFilePersistenceQueue(); - } - } - catch (Exception ex) - { - // Nothing to catch here - we'll just persist - LogHelper.Error("An error occurred checking if xml file is queued for persistence", ex); - } - } - } - return false; - } - } - /// /// Invalidates the disk content cache file. Effectively just deletes it. /// @@ -1248,51 +1196,26 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } } + [Obsolete("This method should not be used, xml file persistence is done in a queue using a BackgroundTaskRunner")] public void PersistXmlToFile() - { - PersistXmlToFile(_xmlContent); - } - - /// - /// Persist a XmlDocument to the Disk Cache - /// - /// - internal void PersistXmlToFile(XmlDocument xmlDoc) { lock (ReaderWriterSyncLock) { - if (xmlDoc != null) - { - LogHelper.Debug("Saving content to disk on thread '{0}' (Threadpool? {1})", - () => Thread.CurrentThread.Name, - () => Thread.CurrentThread.IsThreadPoolThread); - + if (_xmlContent != null) + { try { - Stopwatch stopWatch = Stopwatch.StartNew(); + // create directory for cache path if it doesn't yet exist + var directoryName = Path.GetDirectoryName(UmbracoXmlDiskCacheFileName); + Directory.CreateDirectory(directoryName); - DeleteXmlCache(); - - // Try to create directory for cache path if it doesn't yet exist - string directoryName = Path.GetDirectoryName(UmbracoXmlDiskCacheFileName); - if (!File.Exists(UmbracoXmlDiskCacheFileName) && !Directory.Exists(directoryName)) - { - // We're already in a try-catch and saving will fail if this does, so don't need another - Directory.CreateDirectory(directoryName); - } - - xmlDoc.Save(UmbracoXmlDiskCacheFileName); - - LogHelper.Debug("Saved content on thread '{0}' in {1} (Threadpool? {2})", - () => Thread.CurrentThread.Name, - () => stopWatch.Elapsed, - () => Thread.CurrentThread.IsThreadPoolThread); + _xmlContent.Save(UmbracoXmlDiskCacheFileName); } catch (Exception ee) { // If for whatever reason something goes wrong here, invalidate disk cache DeleteXmlCache(); - + LogHelper.Error(string.Format( "Error saving content on thread '{0}' due to '{1}' (Threadpool? {2})", Thread.CurrentThread.Name, ee.Message, Thread.CurrentThread.IsThreadPoolThread), ee); @@ -1302,48 +1225,18 @@ order by umbracoNode.level, umbracoNode.sortOrder"; } /// - /// Marks a flag in the HttpContext so that, upon page execution completion, the Xml cache will - /// get persisted to disk. Ensure this method is only called from a thread executing a page request - /// since UmbracoModule is the only monitor of this flag and is responsible - /// for enacting the persistence at the PostRequestHandlerExecute stage of the page lifecycle. + /// Adds a task to the xml cache file persister /// private void QueueXmlForPersistence() { - //if this is called outside a web request we cannot queue it it will run in the current thread. - - if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null) - { - UmbracoContext.Current.HttpContext.Application.Lock(); - try - { - if (UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] != null) - { - UmbracoContext.Current.HttpContext.Application.Add(PersistenceFlagContextKey, null); - } - UmbracoContext.Current.HttpContext.Application[PersistenceFlagContextKey] = DateTime.UtcNow; - } - finally - { - UmbracoContext.Current.HttpContext.Application.UnLock(); - } - } - else - { - // Save copy of content - if (UmbracoSettings.CloneXmlCacheOnPublish) - { - XmlDocument xmlContentCopy = CloneXmlDoc(_xmlContent); - PersistXmlToFile(xmlContentCopy); - } - else - { - PersistXmlToFile(); - } - } + FilePersister.Add(new XmlCacheFilePersister(_xmlContent, UmbracoXmlDiskCacheFileName, + new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); } internal DateTime GetCacheFileUpdateTime() { + //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? + if (File.Exists(UmbracoXmlDiskCacheFileName)) { return new FileInfo(UmbracoXmlDiskCacheFileName).LastWriteTimeUtc; diff --git a/src/Umbraco.Web/umbraco.presentation/requestModule.cs b/src/Umbraco.Web/umbraco.presentation/requestModule.cs index 6be1737545..9e2eeff4e6 100644 --- a/src/Umbraco.Web/umbraco.presentation/requestModule.cs +++ b/src/Umbraco.Web/umbraco.presentation/requestModule.cs @@ -317,10 +317,10 @@ namespace umbraco.presentation void context_PostRequestHandlerExecute(object sender, EventArgs e) { - if (content.Instance.IsXmlQueuedForPersistenceToFile) - { - content.Instance.PersistXmlToFile(); - } + //if (content.Instance.IsXmlQueuedForPersistenceToFile) + //{ + // content.Instance.PersistXmlToFile(); + //} } From 912b01c9aa5321674d626e9bb96b773ec39b1e5f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 14:59:33 +1100 Subject: [PATCH 110/249] Updates BackgroundTaskRunner to support more complex options including the ability to only execute the last/final task in the queue. Added tests to support, updated the 'content' object to use this option so that only the last task in the queue will execute so that file persisting doesn't get queued but the correctly queued data will be written. --- .../Scheduling/BackgroundTaskRunnerTests.cs | 24 +++++++ .../Scheduling/BackgroundTaskRunner.cs | 66 ++++++++++++++++--- .../umbraco.presentation/content.cs | 3 +- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 7cc763e534..3e851a68f0 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -83,6 +83,30 @@ namespace Umbraco.Tests.Scheduling } } + [Test] + public async void Many_Tasks_Added_Only_Last_Task_Executes_With_Option() + { + var tasks = new Dictionary(); + for (var i = 0; i < 10; i++) + { + tasks.Add(new MyTask(), new ManualResetEvent(false)); + } + + BackgroundTaskRunner tManager; + using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions{OnlyProcessLastItem = true})) + { + + tasks.ForEach(t => tManager.Add(t.Key)); + + //wait till the thread is done + await tManager; + + var countExecuted = tasks.Count(x => x.Key.Ended != default(DateTime)); + + Assert.AreEqual(1, countExecuted); + } + } + [Test] public void Tasks_Can_Keep_Being_Added_And_Will_Execute() { diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 60eef43b17..d11ff03d66 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -8,6 +8,26 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { + + internal class BackgroundTaskRunnerOptions + { + public BackgroundTaskRunnerOptions() + { + DedicatedThread = false; + PersistentThread = false; + OnlyProcessLastItem = false; + } + + public bool DedicatedThread { get; set; } + public bool PersistentThread { get; set; } + + /// + /// If this is true, the task runner will skip over all items and only process the last/final + /// item registered + /// + public bool OnlyProcessLastItem { get; set; } + } + /// /// This is used to create a background task runner which will stay alive in the background of and complete /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain @@ -17,8 +37,7 @@ namespace Umbraco.Web.Scheduling internal class BackgroundTaskRunner : IDisposable, IRegisteredObject where T : IBackgroundTask { - private readonly bool _dedicatedThread; - private readonly bool _persistentThread; + private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); private Task _consumer; @@ -31,9 +50,15 @@ namespace Umbraco.Web.Scheduling internal event EventHandler> TaskCancelled; public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false) + : this(new BackgroundTaskRunnerOptions{DedicatedThread = dedicatedThread, PersistentThread = persistentThread}) + { + } + + public BackgroundTaskRunner(BackgroundTaskRunnerOptions options) { - _dedicatedThread = dedicatedThread; - _persistentThread = persistentThread; + if (options == null) throw new ArgumentNullException("options"); + _options = options; + HostingEnvironment.RegisterObject(this); } @@ -146,6 +171,13 @@ namespace Umbraco.Web.Scheduling T remainingTask; while (_tasks.TryTake(out remainingTask)) { + //skip if this is not the last + if (_options.OnlyProcessLastItem && _tasks.Count > 0) + { + //NOTE: don't raise canceled event, we're shutting down + continue; + } + ConsumeTaskInternalAsync(remainingTask) .Wait(); //block until it completes } @@ -181,13 +213,13 @@ namespace Umbraco.Web.Scheduling _consumer = Task.Factory.StartNew(() => StartThreadAsync(token), token, - _dedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, + _options.DedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, TaskScheduler.Default); //if this is not a persistent thread, wait till it's done and shut ourselves down // thus ending the thread or giving back to the thread pool. If another task is added // another thread will spawn or be taken from the pool to process. - if (!_persistentThread) + if (!_options.PersistentThread) { _consumer.ContinueWith(task => ShutDown()); } @@ -228,7 +260,7 @@ namespace Umbraco.Web.Scheduling //When this is false, the thread will process what is currently in the queue and once that is // done, the thread will end and we will shutdown the process - if (_persistentThread) + if (_options.PersistentThread) { //This will iterate over the collection, if there is nothing to take // the thread will block until there is something available. @@ -236,6 +268,13 @@ namespace Umbraco.Web.Scheduling // cancel when we shutdown foreach (var t in _tasks.GetConsumingEnumerable(token)) { + //skip if this is not the last + if (_options.OnlyProcessLastItem && _tasks.Count > 0) + { + OnTaskCancelled(new TaskEventArgs(t)); + continue; + } + await ConsumeTaskCancellableAsync(t, token); } @@ -244,10 +283,17 @@ namespace Umbraco.Web.Scheduling } else { - T repositoryTask; - while (_tasks.TryTake(out repositoryTask)) + T t; + while (_tasks.TryTake(out t)) { - await ConsumeTaskCancellableAsync(repositoryTask, token); + //skip if this is not the last + if (_options.OnlyProcessLastItem && _tasks.Count > 0) + { + OnTaskCancelled(new TaskEventArgs(t)); + continue; + } + + await ConsumeTaskCancellableAsync(t, token); } //the task will end here diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 2905abaf99..cd736b98ff 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -32,7 +32,8 @@ namespace umbraco /// public class content { - private static readonly BackgroundTaskRunner FilePersister = new BackgroundTaskRunner(dedicatedThread: true); + private static readonly BackgroundTaskRunner FilePersister + = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions {DedicatedThread = true, OnlyProcessLastItem = true}); #region Declarations From bc068b201d4b937e4ead37df2c10d212f1dc635a Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 15:12:37 +1100 Subject: [PATCH 111/249] Updates BackgroundTaskRunner to ensure canceled or skipped tasks are disposed, updated tests to reflect --- .../Scheduling/BackgroundTaskRunnerTests.cs | 22 ++++++++++------ .../Scheduling/BackgroundTaskRunner.cs | 26 ++++--------------- .../Scheduling/BackgroundTaskRunnerOptions.cs | 23 ++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 4 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 3e851a68f0..11ba81b585 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -273,22 +273,25 @@ namespace Umbraco.Tests.Scheduling { } - public override void Run() + public override void PerformRun() { Thread.Sleep(500); } - public override void Cancel() - { - - } } public abstract class BaseTask : IBackgroundTask { + public bool WasCancelled { get; set; } + public Guid UniqueId { get; protected set; } - public abstract void Run(); + public abstract void PerformRun(); + public void Run() + { + PerformRun(); + Ended = DateTime.Now; + } public Task RunAsync() { throw new NotImplementedException(); @@ -299,7 +302,10 @@ namespace Umbraco.Tests.Scheduling get { return false; } } - public abstract void Cancel(); + public virtual void Cancel() + { + WasCancelled = true; + } public DateTime Queued { get; set; } public DateTime Started { get; set; } @@ -307,7 +313,7 @@ namespace Umbraco.Tests.Scheduling public virtual void Dispose() { - Ended = DateTime.Now; + } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index d11ff03d66..ffba7a1e0a 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -8,26 +8,6 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - - internal class BackgroundTaskRunnerOptions - { - public BackgroundTaskRunnerOptions() - { - DedicatedThread = false; - PersistentThread = false; - OnlyProcessLastItem = false; - } - - public bool DedicatedThread { get; set; } - public bool PersistentThread { get; set; } - - /// - /// If this is true, the task runner will skip over all items and only process the last/final - /// item registered - /// - public bool OnlyProcessLastItem { get; set; } - } - /// /// This is used to create a background task runner which will stay alive in the background of and complete /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain @@ -174,7 +154,8 @@ namespace Umbraco.Web.Scheduling //skip if this is not the last if (_options.OnlyProcessLastItem && _tasks.Count > 0) { - //NOTE: don't raise canceled event, we're shutting down + //NOTE: don't raise canceled event, we're shutting down, just dispose + remainingTask.Dispose(); continue; } @@ -372,6 +353,9 @@ namespace Umbraco.Web.Scheduling { var handler = TaskCancelled; if (handler != null) handler(this, e); + + //dispose it + e.Task.Dispose(); } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs new file mode 100644 index 0000000000..c42fcd681a --- /dev/null +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -0,0 +1,23 @@ +namespace Umbraco.Web.Scheduling +{ + internal class BackgroundTaskRunnerOptions + { + //TODO: Could add options for using a stack vs queue if required + + public BackgroundTaskRunnerOptions() + { + DedicatedThread = false; + PersistentThread = false; + OnlyProcessLastItem = false; + } + + public bool DedicatedThread { get; set; } + public bool PersistentThread { get; set; } + + /// + /// If this is true, the task runner will skip over all items and only process the last/final + /// item registered + /// + public bool OnlyProcessLastItem { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8cd6f55046..23ba69cf31 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -498,6 +498,7 @@ + From b7436dc55f82b25461f2b46c769b325d9519795e Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 6 Feb 2015 16:10:34 +0100 Subject: [PATCH 112/249] refactor BackgroundTaskRunner --- .../Scheduling/BackgroundTaskRunnerTests.cs | 671 ++++++++++++++---- .../Scheduling/BackgroundTaskRunner.cs | 552 ++++++++------ .../Scheduling/BackgroundTaskRunnerOptions.cs | 37 +- .../Scheduling/DelayedRecurringTaskBase.cs | 52 ++ src/Umbraco.Web/Scheduling/IBackgroundTask.cs | 16 + .../Scheduling/IBackgroundTaskRunner.cs | 20 + .../Scheduling/IDelayedBackgroundTask.cs | 23 + .../Scheduling/RecurringTaskBase.cs | 108 +++ .../Scheduling/TaskAndFactoryExtensions.cs | 62 ++ src/Umbraco.Web/Umbraco.Web.csproj | 5 + .../umbraco.presentation/content.cs | 2 +- 11 files changed, 1168 insertions(+), 380 deletions(-) create mode 100644 src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs create mode 100644 src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs create mode 100644 src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs create mode 100644 src/Umbraco.Web/Scheduling/RecurringTaskBase.cs create mode 100644 src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 11ba81b585..e83ce400d9 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -13,97 +13,272 @@ namespace Umbraco.Tests.Scheduling [TestFixture] public class BackgroundTaskRunnerTests { - - - [Test] - public void Startup_And_Shutdown() + private static void AssertRunnerStopsRunning(BackgroundTaskRunner runner, int timeoutMilliseconds = 2000) + where T : class, IBackgroundTask { - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) - { - tManager.StartUp(); - } + const int period = 200; - NUnit.Framework.Assert.IsFalse(tManager.IsRunning); + var i = 0; + var m = timeoutMilliseconds/period; + while (runner.IsRunning && i++ < m) + Thread.Sleep(period); + Assert.IsFalse(runner.IsRunning, "Runner is still running."); } [Test] - public void Startup_Starts_Automatically() + public void ShutdownWaitWhenRunning() { - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) { - tManager.Add(new MyTask()); - NUnit.Framework.Assert.IsTrue(tManager.IsRunning); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // for long + Assert.IsTrue(runner.IsRunning); + runner.Shutdown(false, true); // -force +wait + AssertRunnerStopsRunning(runner); + Assert.IsTrue(runner.IsCompleted); } } [Test] - public void Task_Runs() + public void ShutdownWhenRunning() { - var myTask = new MyTask(); - var waitHandle = new ManualResetEvent(false); - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - tManager.TaskCompleted += (sender, task) => waitHandle.Set(); + // do NOT try to do this because the code must run on the UI thread which + // is not availably, and so the thread never actually starts - wondering + // what it means for ASP.NET? + //runner.TaskStarting += (sender, args) => Console.WriteLine("starting {0:c}", DateTime.Now); + //runner.TaskCompleted += (sender, args) => Console.WriteLine("completed {0:c}", DateTime.Now); - tManager.Add(myTask); - - //wait for ITasks to complete - waitHandle.WaitOne(); - - NUnit.Framework.Assert.IsTrue(myTask.Ended != default(DateTime)); + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + Assert.IsTrue(runner.IsRunning); // is running the task + runner.Shutdown(false, false); // -force -wait + Assert.IsTrue(runner.IsCompleted); + Assert.IsTrue(runner.IsRunning); // still running that task + Thread.Sleep(3000); + Assert.IsTrue(runner.IsRunning); // still running that task + AssertRunnerStopsRunning(runner, 10000); } } [Test] - public void Many_Tasks_Run() + public void ShutdownFlushesTheQueue() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + var t = new MyTask(); + runner.Add(t); + Assert.IsTrue(runner.IsRunning); // is running the first task + runner.Shutdown(false, false); // -force -wait + AssertRunnerStopsRunning(runner, 10000); + Assert.AreNotEqual(DateTime.MinValue, t.Ended); // t has run + } + } + + [Test] + public void ShutdownForceTruncatesTheQueue() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + var t = new MyTask(); + runner.Add(t); + Assert.IsTrue(runner.IsRunning); // is running the first task + runner.Shutdown(true, false); // +force -wait + AssertRunnerStopsRunning(runner, 10000); + Assert.AreEqual(DateTime.MinValue, t.Ended); // t has not run + } + } + + [Test] + public void ShutdownThenForce() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.Add(new MyTask(5000)); + runner.Add(new MyTask()); + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); // is running the task + runner.Shutdown(false, false); // -force -wait + Assert.IsTrue(runner.IsCompleted); + Assert.IsTrue(runner.IsRunning); // still running that task + Thread.Sleep(3000); + Assert.IsTrue(runner.IsRunning); // still running that task + runner.Shutdown(true, false); // +force -wait + AssertRunnerStopsRunning(runner, 20000); + } + } + + [Test] + public void Create_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + } + } + + [Test] + public void Create_AutoStart_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true })) + { + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public void Create_AutoStartAndKeepAlive_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) + { + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // for long + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public void Dispose_IsRunning() + { + BackgroundTaskRunner runner; + using (runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { AutoStart = true, KeepAlive = true })) + { + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + + AssertRunnerStopsRunning(runner); + Assert.Throws(() => runner.Add(new MyTask())); + } + + [Test] + public void Startup_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + Assert.IsFalse(runner.IsRunning); + runner.StartUp(); + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public void Startup_KeepAlive_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true })) + { + Assert.IsFalse(runner.IsRunning); + runner.StartUp(); + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public void Create_AddTask_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // task takes 500ms + Assert.IsFalse(runner.IsRunning); + } + } + + [Test] + public void Create_KeepAliveAndAddTask_IsRunning() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = true })) + { + runner.Add(new MyTask()); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(800); // task takes 500ms + Assert.IsTrue(runner.IsRunning); + // dispose will stop it + } + } + + [Test] + public async void WaitOnRunner_OneTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyTask(); + Assert.IsTrue(task.Ended == default(DateTime)); + runner.Add(task); + await runner; // wait 'til it's not running anymore + Assert.IsTrue(task.Ended != default(DateTime)); // task is done + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public async void WaitOnRunner_Tasks() + { + var tasks = new List(); + for (var i = 0; i < 10; i++) + tasks.Add(new MyTask()); + + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { KeepAlive = false, LongRunning = true, PreserveRunningTask = true })) + { + tasks.ForEach(runner.Add); + + await runner; // wait 'til it's not running anymore + + // check that tasks are done + Assert.IsTrue(tasks.All(x => x.Ended != default(DateTime))); + + Assert.AreEqual(TaskStatus.RanToCompletion, runner.TaskStatus); + Assert.IsFalse(runner.IsRunning); + Assert.IsFalse(runner.IsDisposed); + } + } + + [Test] + public void WaitOnTask() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyTask(); + var waitHandle = new ManualResetEvent(false); + runner.TaskCompleted += (sender, t) => waitHandle.Set(); + Assert.IsTrue(task.Ended == default(DateTime)); + runner.Add(task); + waitHandle.WaitOne(); // wait 'til task is done + Assert.IsTrue(task.Ended != default(DateTime)); // task is done + AssertRunnerStopsRunning(runner); // though not for long + } + } + + [Test] + public void WaitOnTasks() { var tasks = new Dictionary(); for (var i = 0; i < 10; i++) - { tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { - tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + runner.TaskCompleted += (sender, task) => tasks[task.Task].Set(); + foreach (var t in tasks) runner.Add(t.Key); - tasks.ForEach(t => tManager.Add(t.Key)); - - //wait for all ITasks to complete + // wait 'til tasks are done, check that tasks are done WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); + Assert.IsTrue(tasks.All(x => x.Key.Ended != default(DateTime))); - foreach (var task in tasks) - { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); - } - } - } - - [Test] - public async void Many_Tasks_Added_Only_Last_Task_Executes_With_Option() - { - var tasks = new Dictionary(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions{OnlyProcessLastItem = true})) - { - - tasks.ForEach(t => tManager.Add(t.Key)); - - //wait till the thread is done - await tManager; - - var countExecuted = tasks.Count(x => x.Key.Ended != default(DateTime)); - - Assert.AreEqual(1, countExecuted); + AssertRunnerStopsRunning(runner); // though not for long } } @@ -123,7 +298,7 @@ namespace Umbraco.Tests.Scheduling IDictionary tasks = getTasks(); BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) + using (tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, KeepAlive = true })) { tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); @@ -135,7 +310,7 @@ namespace Umbraco.Tests.Scheduling foreach (var task in tasks) { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + Assert.IsTrue(task.Key.Ended != default(DateTime)); } //execute another batch after a bit @@ -149,71 +324,11 @@ namespace Umbraco.Tests.Scheduling foreach (var task in tasks) { - NUnit.Framework.Assert.IsTrue(task.Key.Ended != default(DateTime)); + Assert.IsTrue(task.Key.Ended != default(DateTime)); } } } - [Test] - public void Task_Queue_Will_Be_Completed_Before_Shutdown() - { - var tasks = new Dictionary(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask(), new ManualResetEvent(false)); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(true, true)) - { - tManager.TaskCompleted += (sender, task) => tasks[task.Task].Set(); - - tasks.ForEach(t => tManager.Add(t.Key)); - - ////wait for all ITasks to complete - //WaitHandle.WaitAll(tasks.Values.Select(x => (WaitHandle)x).ToArray()); - - tManager.Stop(false); - //immediate stop will block until complete - but since we are running on - // a single thread this doesn't really matter as the above will just process - // until complete. - tManager.Stop(true); - - NUnit.Framework.Assert.AreEqual(0, tManager.TaskCount); - } - } - - //NOTE: These tests work in .Net 4.5 but in this current version we don't have the correct - // async/await signatures with GetAwaiter, so am just commenting these out in this version - - [Test] - public async void Non_Persistent_Runner_Will_End_After_Queue_Empty() - { - var tasks = new List(); - for (var i = 0; i < 10; i++) - { - tasks.Add(new MyTask()); - } - - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread:true)) - { - tasks.ForEach(t => tManager.Add(t)); - - //wait till the thread is done - await tManager; - - foreach (var task in tasks) - { - Assert.IsTrue(task.Ended != default(DateTime)); - } - - Assert.AreEqual(TaskStatus.RanToCompletion, tManager.TaskStatus); - Assert.IsFalse(tManager.IsRunning); - Assert.IsFalse(tManager.IsDisposed); - } - } - [Test] public async void Non_Persistent_Runner_Will_Start_New_Threads_When_Required() { @@ -229,10 +344,9 @@ namespace Umbraco.Tests.Scheduling List tasks = getTasks(); - BackgroundTaskRunner tManager; - using (tManager = new BackgroundTaskRunner(persistentThread: false, dedicatedThread: true)) + using (var tManager = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true, PreserveRunningTask = true })) { - tasks.ForEach(t => tManager.Add(t)); + tasks.ForEach(tManager.Add); //wait till the thread is done await tManager; @@ -250,7 +364,7 @@ namespace Umbraco.Tests.Scheduling tasks = getTasks(); //add more tasks - tasks.ForEach(t => tManager.Add(t)); + tasks.ForEach(tManager.Add); //wait till the thread is done await tManager; @@ -265,19 +379,296 @@ namespace Umbraco.Tests.Scheduling Assert.IsFalse(tManager.IsDisposed); } } - - private class MyTask : BaseTask + [Test] + public void RecurringTaskTest() { - public MyTask() + // note: can have BackgroundTaskRunner and use it in MyRecurringTask ctor + // because that ctor wants IBackgroundTaskRunner and the generic type + // parameter is contravariant ie defined as IBackgroundTaskRunner so doing the + // following is legal: + // var IBackgroundTaskRunner b = ...; + // var IBackgroundTaskRunner d = b; // legal + + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) { + var task = new MyRecurringTask(runner, 200, 500); + MyRecurringTask.RunCount = 0; + runner.Add(task); + Thread.Sleep(5000); + Assert.GreaterOrEqual(MyRecurringTask.RunCount, 2); // keeps running, count >= 2 + + // stops recurring + runner.Shutdown(false, false); + AssertRunnerStopsRunning(runner); + + // timer may try to add a task but it won't work because runner is completed + } + } + + [Test] + public void DelayedTaskRuns() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyDelayedTask(200); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(5000); + Assert.IsTrue(runner.IsRunning); // still waiting for the task to release + Assert.IsFalse(task.HasRun); + task.Release(); + Thread.Sleep(500); + Assert.IsTrue(task.HasRun); + AssertRunnerStopsRunning(runner); // runs task & exit + } + } + + [Test] + public void DelayedTaskStops() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyDelayedTask(200); + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + Thread.Sleep(5000); + Assert.IsTrue(runner.IsRunning); // still waiting for the task to release + Assert.IsFalse(task.HasRun); + runner.Shutdown(false, false); + AssertRunnerStopsRunning(runner); // runs task & exit + Assert.IsTrue(task.HasRun); + } + } + + [Test] + public void DelayedRecurring() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var task = new MyDelayedRecurringTask(runner, 2000, 1000); + MyDelayedRecurringTask.RunCount = 0; + runner.Add(task); + Thread.Sleep(1000); + Assert.IsTrue(runner.IsRunning); // waiting on delay + Assert.AreEqual(0, MyDelayedRecurringTask.RunCount); + Thread.Sleep(1000); + Assert.AreEqual(1, MyDelayedRecurringTask.RunCount); + Thread.Sleep(5000); + Assert.GreaterOrEqual(MyDelayedRecurringTask.RunCount, 2); // keeps running, count >= 2 + + // stops recurring + runner.Shutdown(false, false); + AssertRunnerStopsRunning(runner); + + // timer may try to add a task but it won't work because runner is completed + } + } + + [Test] + public void FailingTaskSync() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var exceptions = new ConcurrentQueue(); + runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); + + var task = new MyFailingTask(false); // -async + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // runs task & exit + + Assert.AreEqual(1, exceptions.Count); // traced and reported + } + } + + [Test] + public void FailingTaskAsync() + { + using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions())) + { + var exceptions = new ConcurrentQueue(); + runner.TaskError += (sender, args) => exceptions.Enqueue(args.Exception); + + var task = new MyFailingTask(true); // +async + runner.Add(task); + Assert.IsTrue(runner.IsRunning); + AssertRunnerStopsRunning(runner); // runs task & exit + + Assert.AreEqual(1, exceptions.Count); // traced and reported + } + } + + private class MyFailingTask : IBackgroundTask + { + private readonly bool _isAsync; + + public MyFailingTask(bool isAsync) + { + _isAsync = isAsync; + } + + public void Run() + { + Thread.Sleep(1000); + throw new Exception("Task has thrown."); + } + + public async Task RunAsync() + { + await Task.Delay(1000); + throw new Exception("Task has thrown."); + } + + public bool IsAsync + { + get { return _isAsync; } + } + + // fixme - must also test what happens if we throw on dispose! + public void Dispose() + { } + } + + private class MyDelayedRecurringTask : DelayedRecurringTaskBase + { + public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + : base(runner, delayMilliseconds, periodMilliseconds) + { } + + private MyDelayedRecurringTask(MyDelayedRecurringTask source) + : base(source) + { } + + public static int RunCount { get; set; } + + public override bool IsAsync + { + get { return false; } } public override void PerformRun() { - Thread.Sleep(500); + // nothing to do at the moment + RunCount += 1; } + public override Task PerformRunAsync() + { + throw new NotImplementedException(); + } + + protected override MyDelayedRecurringTask GetRecurring() + { + return new MyDelayedRecurringTask(this); + } + } + + private class MyDelayedTask : IDelayedBackgroundTask + { + private readonly int _runMilliseconds; + private readonly ManualResetEvent _gate; + + public bool HasRun { get; private set; } + + public MyDelayedTask(int runMilliseconds) + { + _runMilliseconds = runMilliseconds; + _gate = new ManualResetEvent(false); + } + + public WaitHandle DelayWaitHandle + { + get { return _gate; } + } + + public bool IsDelayed + { + get { return true; } + } + + public void Run() + { + Thread.Sleep(_runMilliseconds); + HasRun = true; + } + + public void Release() + { + _gate.Set(); + } + + public Task RunAsync() + { + throw new NotImplementedException(); + } + + public bool IsAsync + { + get { return false; } + } + + public void Dispose() + { } + } + + private class MyRecurringTask : RecurringTaskBase + { + private readonly int _runMilliseconds; + + public static int RunCount { get; set; } + + public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) + : base(runner, periodMilliseconds) + { + _runMilliseconds = runMilliseconds; + } + + private MyRecurringTask(MyRecurringTask source, int runMilliseconds) + : base(source) + { + _runMilliseconds = runMilliseconds; + } + + public override void PerformRun() + { + RunCount += 1; + Thread.Sleep(_runMilliseconds); + } + + public override Task PerformRunAsync() + { + throw new NotImplementedException(); + } + + public override bool IsAsync + { + get { return false; } + } + + protected override MyRecurringTask GetRecurring() + { + return new MyRecurringTask(this, _runMilliseconds); + } + } + + private class MyTask : BaseTask + { + private readonly int _milliseconds; + + public MyTask() + : this(500) + { } + + public MyTask(int milliseconds) + { + _milliseconds = milliseconds; + } + + public override void PerformRun() + { + Thread.Sleep(_milliseconds); + } } public abstract class BaseTask : IBackgroundTask @@ -287,14 +678,17 @@ namespace Umbraco.Tests.Scheduling public Guid UniqueId { get; protected set; } public abstract void PerformRun(); + public void Run() { PerformRun(); Ended = DateTime.Now; } + public Task RunAsync() { throw new NotImplementedException(); + //return Task.Delay(500); // fixme } public bool IsAsync @@ -312,10 +706,7 @@ namespace Umbraco.Tests.Scheduling public DateTime Ended { get; set; } public virtual void Dispose() - { - - } + { } } - } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index ffba7a1e0a..c511a0af53 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -9,60 +10,97 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { /// - /// This is used to create a background task runner which will stay alive in the background of and complete - /// any tasks that are queued. It is web aware and will ensure that it is shutdown correctly when the app domain - /// is shutdown. + /// Manages a queue of tasks of type and runs them in the background. /// - /// - internal class BackgroundTaskRunner : IDisposable, IRegisteredObject - where T : IBackgroundTask + /// The type of the managed tasks. + /// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain + /// shuts down (ie is unloaded). FIXME WHAT DOES THAT MEAN? + internal class BackgroundTaskRunner : IBackgroundTaskRunner + where T : class, IBackgroundTask { private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); - private Task _consumer; + private readonly object _locker = new object(); + private readonly ManualResetEvent _completedEvent = new ManualResetEvent(false); + + private volatile bool _isRunning; // is running + private volatile bool _isCompleted; // does not accept tasks anymore, may still be running + private Task _runningTask; - private volatile bool _isRunning = false; - private static readonly object Locker = new object(); private CancellationTokenSource _tokenSource; + internal event EventHandler> TaskError; internal event EventHandler> TaskStarting; internal event EventHandler> TaskCompleted; internal event EventHandler> TaskCancelled; - public BackgroundTaskRunner(bool dedicatedThread = false, bool persistentThread = false) - : this(new BackgroundTaskRunnerOptions{DedicatedThread = dedicatedThread, PersistentThread = persistentThread}) - { - } + /// + /// Initializes a new instance of the class. + /// + public BackgroundTaskRunner() + : this(new BackgroundTaskRunnerOptions()) + { } + /// + /// Initializes a new instance of the class with a set of options. + /// + /// The set of options. public BackgroundTaskRunner(BackgroundTaskRunnerOptions options) { if (options == null) throw new ArgumentNullException("options"); _options = options; HostingEnvironment.RegisterObject(this); + + if (options.AutoStart) + StartUp(); } + /// + /// Gets the number of tasks in the queue. + /// public int TaskCount { get { return _tasks.Count; } } + /// + /// Gets a value indicating whether a task is currently running. + /// public bool IsRunning { get { return _isRunning; } } - public TaskStatus TaskStatus + /// + /// Gets a value indicating whether the runner has completed and cannot accept tasks anymore. + /// + public bool IsCompleted { - get { return _consumer.Status; } + get { return _isCompleted; } } + /// + /// Gets the status of the running task. + /// + /// There is no running task. + /// Unless the AutoStart option is true, there will be no running task until + /// a background task is added to the queue. Unless the KeepAlive option is true, there + /// will be no running task when the queue is empty. + public TaskStatus TaskStatus + { + get + { + if (_runningTask == null) + throw new InvalidOperationException("There is no current task."); + return _runningTask.Status; + } + } /// - /// Returns the task awaiter so that consumers of the BackgroundTaskManager can await - /// the threading operation. + /// Gets an awaiter used to await the running task. /// - /// + /// An awaiter for the running task. /// /// This is just the coolest thing ever, check this article out: /// http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx @@ -70,267 +108,287 @@ namespace Umbraco.Web.Scheduling /// So long as we have a method called GetAwaiter() that returns an instance of INotifyCompletion /// we can await anything! :) /// + /// There is no running task. + /// Unless the AutoStart option is true, there will be no running task until + /// a background task is added to the queue. Unless the KeepAlive option is true, there + /// will be no running task when the queue is empty. public TaskAwaiter GetAwaiter() { - return _consumer.GetAwaiter(); + if (_runningTask == null) + throw new InvalidOperationException("There is no current task."); + return _runningTask.GetAwaiter(); } + /// + /// Adds a task to the queue. + /// + /// The task to add. + /// The task runner has completed. public void Add(T task) { - //add any tasks first - LogHelper.Debug>(" Task added {0}", () => task.GetType()); - _tasks.Add(task); + lock (_locker) + { + if (_isCompleted) + throw new InvalidOperationException("The task runner has completed."); - //ensure's everything is started - StartUp(); + // add task + LogHelper.Debug>("Task added {0}", task.GetType); + _tasks.Add(task); + + // start + StartUpLocked(); + } } + /// + /// Tries to add a task to the queue. + /// + /// The task to add. + /// true if the task could be added to the queue; otherwise false. + /// Returns false if the runner is completed. + public bool TryAdd(T task) + { + lock (_locker) + { + if (_isCompleted) return false; + + // add task + LogHelper.Debug>("Task added {0}", task.GetType); + _tasks.Add(task); + + // start + StartUpLocked(); + + return true; + } + } + + /// + /// Starts the tasks runner, if not already running. + /// + /// Is invoked each time a task is added, to ensure it is going to be processed. + /// The task runner has completed. public void StartUp() { - if (!_isRunning) + if (_isRunning) return; + + lock (_locker) { - lock (Locker) - { - //double check - if (!_isRunning) - { - _isRunning = true; - //Create a new token source since this is a new proces - _tokenSource = new CancellationTokenSource(); - StartConsumer(); - LogHelper.Debug>("Starting"); - } - } - } - } + if (_isCompleted) + throw new InvalidOperationException("The task runner has completed."); - public void ShutDown() - { - lock (Locker) - { - _isRunning = false; - - try - { - if (_consumer != null) - { - //cancel all operations - _tokenSource.Cancel(); - - try - { - _consumer.Wait(); - } - catch (AggregateException e) - { - //NOTE: We are logging Debug because we are expecting these errors - - LogHelper.Debug>("AggregateException thrown with the following inner exceptions:"); - // Display information about each exception. - foreach (var v in e.InnerExceptions) - { - var exception = v as TaskCanceledException; - if (exception != null) - { - LogHelper.Debug>(" .Net TaskCanceledException: .Net Task ID {0}", () => exception.Task.Id); - } - else - { - LogHelper.Debug>(" Exception: {0}", () => v.GetType().Name); - } - } - } - } - - if (_tasks.Count > 0) - { - LogHelper.Debug>("Processing remaining tasks before shutdown: {0}", () => _tasks.Count); - - //now we need to ensure the remaining queue is processed if there's any remaining, - // this will all be processed on the current/main thread. - T remainingTask; - while (_tasks.TryTake(out remainingTask)) - { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - //NOTE: don't raise canceled event, we're shutting down, just dispose - remainingTask.Dispose(); - continue; - } - - ConsumeTaskInternalAsync(remainingTask) - .Wait(); //block until it completes - } - } - - LogHelper.Debug>("Shutdown"); - - //disposing these is really optional since they'll be disposed immediately since they are no longer running - //but we'll put this here anyways. - if (_consumer != null && (_consumer.IsCompleted || _consumer.IsCanceled)) - { - _consumer.Dispose(); - } - } - catch (Exception ex) - { - LogHelper.Error>("Error occurred shutting down task runner", ex); - } - finally - { - HostingEnvironment.UnregisterObject(this); - } + StartUpLocked(); } } /// - /// Starts the consumer task + /// Starts the tasks runner, if not already running. /// - private void StartConsumer() + /// Must be invoked within lock(_locker) and with _isCompleted being false. + private void StartUpLocked() { - var token = _tokenSource.Token; - - _consumer = Task.Factory.StartNew(() => - StartThreadAsync(token), - token, - _options.DedicatedThread ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, - TaskScheduler.Default); - - //if this is not a persistent thread, wait till it's done and shut ourselves down - // thus ending the thread or giving back to the thread pool. If another task is added - // another thread will spawn or be taken from the pool to process. - if (!_options.PersistentThread) - { - _consumer.ContinueWith(task => ShutDown()); - } + // double check + if (_isRunning) return; + _isRunning = true; + // create a new token source since this is a new process + _tokenSource = new CancellationTokenSource(); + _runningTask = PumpIBackgroundTasks(Task.Factory, _tokenSource.Token); + LogHelper.Debug>("Starting"); } /// - /// Invokes a new worker thread to consume tasks + /// Shuts the taks runner down. /// - /// - private async Task StartThreadAsync(CancellationToken token) + /// True for force the runner to stop. + /// True to wait until the runner has stopped. + /// If is false, no more tasks can be queued but all queued tasks + /// will run. If it is true, then only the current one (if any) will end and no other task will run. + public void Shutdown(bool force, bool wait) { - // Was cancellation already requested? - if (token.IsCancellationRequested) + lock (_locker) { - LogHelper.Info>("Thread {0} was cancelled before it got started.", () => Thread.CurrentThread.ManagedThreadId); - token.ThrowIfCancellationRequested(); + _isCompleted = true; // do not accept new tasks + if (_isRunning == false) return; // done already } - await TakeAndConsumeTaskAsync(token); - } + // try to be nice + // assuming multiple threads can do these without problems + _completedEvent.Set(); + _tasks.CompleteAdding(); - /// - /// Trys to get a task from the queue, if there isn't one it will wait a second and try again - /// - /// - private async Task TakeAndConsumeTaskAsync(CancellationToken token) - { - if (token.IsCancellationRequested) + if (force) { - LogHelper.Info>("Thread {0} was cancelled.", () => Thread.CurrentThread.ManagedThreadId); - token.ThrowIfCancellationRequested(); - } - - //If this is true, the thread will stay alive and just wait until there is anything in the queue - // and process it. When there is nothing in the queue, the thread will just block until there is - // something to process. - //When this is false, the thread will process what is currently in the queue and once that is - // done, the thread will end and we will shutdown the process - - if (_options.PersistentThread) - { - //This will iterate over the collection, if there is nothing to take - // the thread will block until there is something available. - //We need to pass our cancellation token so that the thread will - // cancel when we shutdown - foreach (var t in _tasks.GetConsumingEnumerable(token)) + // we must bring everything down, now + Thread.Sleep(100); // give time to CompleAdding() + lock (_locker) { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - OnTaskCancelled(new TaskEventArgs(t)); - continue; - } - - await ConsumeTaskCancellableAsync(t, token); + // was CompleteAdding() enough? + if (_isRunning == false) return; } - - //recurse and keep going - await TakeAndConsumeTaskAsync(token); + // try to cancel running async tasks (cannot do much about sync tasks) + // break delayed tasks delay + // truncate running queues + _tokenSource.Cancel(false); // false is the default } else { - T t; - while (_tasks.TryTake(out t)) - { - //skip if this is not the last - if (_options.OnlyProcessLastItem && _tasks.Count > 0) - { - OnTaskCancelled(new TaskEventArgs(t)); - continue; - } - - await ConsumeTaskCancellableAsync(t, token); - } - - //the task will end here + // tasks in the queue will be executed... + if (wait == false) return; + _runningTask.Wait(); // wait for whatever is running to end... } } - internal async Task ConsumeTaskCancellableAsync(T task, CancellationToken token) + /// + /// Runs background tasks for as long as there are background tasks in the queue, with an asynchronous operation. + /// + /// The supporting . + /// A cancellation token. + /// The asynchronous operation. + private Task PumpIBackgroundTasks(TaskFactory factory, CancellationToken token) + { + var taskSource = new TaskCompletionSource(factory.CreationOptions); + var enumerator = _options.KeepAlive ? _tasks.GetConsumingEnumerable(token).GetEnumerator() : null; + + // ReSharper disable once MethodSupportsCancellation // always run + var taskSourceContinuing = taskSource.Task.ContinueWith(t => + { + // because the pump does not lock, there's a race condition, + // the pump may stop and then we still have tasks to process, + // and then we must restart the pump - lock to avoid race cond + lock (_locker) + { + if (token.IsCancellationRequested || _tasks.Count == 0) + { + _isRunning = false; // done + if (_options.PreserveRunningTask == false) + _runningTask = null; + return; + } + } + + // if _runningTask is taskSource.Task then we must keep continuing it, + // not starting a new taskSource, else _runningTask would complete and + // something may be waiting on it + //PumpIBackgroundTasks(factory, token); // restart + // ReSharper disable once MethodSupportsCancellation // always run + t.ContinueWithTask(_ => PumpIBackgroundTasks(factory, token)); // restart + }); + + Action pump = null; + pump = task => + { + // RunIBackgroundTaskAsync does NOT throw exceptions, just raises event + // so if we have an exception here, really, wtf? - must read the exception + // anyways so it does not bubble up and kill everything + if (task != null && task.IsFaulted) + { + var exception = task.Exception; + LogHelper.Error>("Task runner exception.", exception); + } + + // is it ok to run? + if (TaskSourceCanceled(taskSource, token)) return; + + // try to get a task + // the blocking MoveNext will end if token is cancelled or collection is completed + T bgTask; + var hasBgTask = _options.KeepAlive + ? (bgTask = enumerator.MoveNext() ? enumerator.Current : null) != null // blocking + : _tasks.TryTake(out bgTask); // non-blocking + + // no task, signal the runner we're done + if (hasBgTask == false) + { + TaskSourceCompleted(taskSource, token); + return; + } + + // wait for delayed task, supporting cancellation + var dbgTask = bgTask as IDelayedBackgroundTask; + if (dbgTask != null && dbgTask.IsDelayed) + { + WaitHandle.WaitAny(new[] { dbgTask.DelayWaitHandle, token.WaitHandle, _completedEvent }); + if (TaskSourceCanceled(taskSource, token)) return; + // else run now, either because delay is ok or runner is completed + } + + // run the task as first task, or a continuation + task = task == null + ? RunIBackgroundTaskAsync(bgTask, token) + // ReSharper disable once MethodSupportsCancellation // always run + : task.ContinueWithTask(_ => RunIBackgroundTaskAsync(bgTask, token)); + + // and pump + // ReSharper disable once MethodSupportsCancellation // always run + task.ContinueWith(t => pump(t)); + }; + + // start it all + factory.StartNew(() => pump(null), + token, + _options.LongRunning ? TaskCreationOptions.LongRunning : TaskCreationOptions.None, + TaskScheduler.Default); + + return taskSourceContinuing; + } + + private bool TaskSourceCanceled(TaskCompletionSource taskSource, CancellationToken token) { if (token.IsCancellationRequested) { - OnTaskCancelled(new TaskEventArgs(task)); - - //NOTE: Since the task hasn't started this is pretty pointless so leaving it out. - LogHelper.Info>("Task {0}) was cancelled.", - () => task.GetType()); - - token.ThrowIfCancellationRequested(); + taskSource.SetCanceled(); + return true; } - - await ConsumeTaskInternalAsync(task); + return false; } - private async Task ConsumeTaskInternalAsync(T task) + private void TaskSourceCompleted(TaskCompletionSource taskSource, CancellationToken token) + { + if (token.IsCancellationRequested) + taskSource.SetCanceled(); + else + taskSource.SetResult(null); + } + + /// + /// Runs a background task asynchronously. + /// + /// The background task. + /// A cancellation token. + /// The asynchronous operation. + internal async Task RunIBackgroundTaskAsync(T bgTask, CancellationToken token) { try { - OnTaskStarting(new TaskEventArgs(task)); + OnTaskStarting(new TaskEventArgs(bgTask)); try { - using (task) + using (bgTask) // ensure it's disposed { - if (task.IsAsync) - { - await task.RunAsync(); - } + if (bgTask.IsAsync) + await bgTask.RunAsync(); // fixme should pass the token along?! else - { - task.Run(); - } + bgTask.Run(); } } catch (Exception e) { - OnTaskError(new TaskEventArgs(task, e)); + OnTaskError(new TaskEventArgs(bgTask, e)); throw; } - - OnTaskCompleted(new TaskEventArgs(task)); + Console.WriteLine("!1"); + OnTaskCompleted(new TaskEventArgs(bgTask)); } catch (Exception ex) { - LogHelper.Error>("An error occurred consuming task", ex); + LogHelper.Error>("Task has failed.", ex); } } + #region Events + protected virtual void OnTaskError(TaskEventArgs e) { var handler = TaskError; @@ -358,8 +416,10 @@ namespace Umbraco.Web.Scheduling e.Task.Dispose(); } + #endregion + + #region IDisposable - #region Disposal private readonly object _disposalLocker = new object(); public bool IsDisposed { get; private set; } @@ -376,8 +436,9 @@ namespace Umbraco.Web.Scheduling protected virtual void Dispose(bool disposing) { - if (this.IsDisposed || !disposing) + if (this.IsDisposed || disposing == false) return; + lock (_disposalLocker) { if (IsDisposed) @@ -389,31 +450,60 @@ namespace Umbraco.Web.Scheduling protected virtual void DisposeResources() { - ShutDown(); + // just make sure we eventually go down + Shutdown(true, false); } + #endregion + /// + /// Requests a registered object to unregister. + /// + /// true to indicate the registered object should unregister from the hosting + /// environment before returning; otherwise, false. + /// + /// "When the application manager needs to stop a registered object, it will call the Stop method." + /// The application manager will call the Stop method to ask a registered object to unregister. During + /// processing of the Stop method, the registered object must call the HostingEnvironment.UnregisterObject method. + /// public void Stop(bool immediate) { if (immediate == false) { - LogHelper.Debug>("Application is shutting down, waiting for tasks to complete"); - Dispose(); + // The Stop method is first called with the immediate parameter set to false. The object can either complete + // processing, call the UnregisterObject method, and then return or it can return immediately and complete + // processing asynchronously before calling the UnregisterObject method. + + LogHelper.Debug>("Shutting down, waiting for tasks to complete."); + Shutdown(false, false); // do not accept any more tasks, flush the queue, do not wait + + lock (_locker) + { + if (_runningTask != null) + _runningTask.ContinueWith(_ => + { + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down, tasks completed."); + }); + else + { + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down, tasks completed."); + } + } } else { - //NOTE: this will thread block the current operation if the manager - // is still shutting down because the Shutdown operation is also locked - // by this same lock instance. This would only matter if Stop is called by ASP.Net - // on two different threads though, otherwise the current thread will just block normally - // until the app is shutdown - lock (Locker) - { - LogHelper.Info>("Application is shutting down immediately"); - } + // If the registered object does not complete processing before the application manager's time-out + // period expires, the Stop method is called again with the immediate parameter set to true. When the + // immediate parameter is true, the registered object must call the UnregisterObject method before returning; + // otherwise, its registration will be removed by the application manager. + + LogHelper.Info>("Shutting down immediately."); + Shutdown(true, true); // cancel all tasks, wait for the current one to end + HostingEnvironment.UnregisterObject(this); + LogHelper.Info>("Down."); } - } - } } diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs index c42fcd681a..4688ff37d6 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunnerOptions.cs @@ -1,23 +1,44 @@ namespace Umbraco.Web.Scheduling { + /// + /// Provides options to the class. + /// internal class BackgroundTaskRunnerOptions { //TODO: Could add options for using a stack vs queue if required + /// + /// Initializes a new instance of the class. + /// public BackgroundTaskRunnerOptions() { - DedicatedThread = false; - PersistentThread = false; - OnlyProcessLastItem = false; + LongRunning = false; + KeepAlive = false; + AutoStart = false; } - public bool DedicatedThread { get; set; } - public bool PersistentThread { get; set; } + /// + /// Gets or sets a value indicating whether the running task should be a long-running, + /// coarse grained operation. + /// + public bool LongRunning { get; set; } /// - /// If this is true, the task runner will skip over all items and only process the last/final - /// item registered + /// Gets or sets a value indicating whether the running task should block and wait + /// on the queue, or end, when the queue is empty. /// - public bool OnlyProcessLastItem { get; set; } + public bool KeepAlive { get; set; } + + /// + /// Gets or sets a value indicating whether the running task should start immediately + /// or only once a task has been added to the queue. + /// + public bool AutoStart { get; set; } + + /// + /// Gets or setes a value indicating whether the running task should be preserved + /// once completed, or reset to null. For unit tests. + /// + public bool PreserveRunningTask { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs new file mode 100644 index 0000000000..573adeda3d --- /dev/null +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Provides a base class for recurring background tasks. + /// + /// The type of the managed tasks. + internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, IDelayedBackgroundTask + where T : class, IBackgroundTask + { + private readonly int _delayMilliseconds; + private ManualResetEvent _gate; + private Timer _timer; + + protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + : base(runner, periodMilliseconds) + { + _delayMilliseconds = delayMilliseconds; + } + + protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) + : base(source) + { + _delayMilliseconds = 0; + } + + public WaitHandle DelayWaitHandle + { + get + { + if (_delayMilliseconds == 0) return new ManualResetEvent(true); + + if (_gate != null) return _gate; + _gate = new ManualResetEvent(false); + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _gate.Set(); + }, null, _delayMilliseconds, 0); + return _gate; + } + } + + public bool IsDelayed + { + get { return _delayMilliseconds > 0; } + } + } +} diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs index 48522aeb5f..9be2512d01 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -3,10 +3,26 @@ using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { + /// + /// Represents a background task. + /// internal interface IBackgroundTask : IDisposable { + /// + /// Runs the background task. + /// void Run(); + + /// + /// Runs the task asynchronously. + /// + /// A instance representing the execution of the background task. + /// The background task cannot run asynchronously. Task RunAsync(); + + /// + /// Indicates whether the background task can run asynchronously. + /// bool IsAsync { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs new file mode 100644 index 0000000000..c4e2dab35d --- /dev/null +++ b/src/Umbraco.Web/Scheduling/IBackgroundTaskRunner.cs @@ -0,0 +1,20 @@ +using System; +using System.Web.Hosting; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Defines a service managing a queue of tasks of type and running them in the background. + /// + /// The type of the managed tasks. + /// The interface is not complete and exists only to have the contravariance on T. + internal interface IBackgroundTaskRunner : IDisposable, IRegisteredObject + where T : class, IBackgroundTask + { + bool IsCompleted { get; } + void Add(T task); + bool TryAdd(T task); + + // fixme - complete the interface? + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs new file mode 100644 index 0000000000..01f8a5e01a --- /dev/null +++ b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs @@ -0,0 +1,23 @@ +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Represents a delayed background task. + /// + /// Delayed background tasks can suspend their execution until + /// a condition is met. However if the tasks runner has to terminate, + /// delayed background tasks are executed immediately. + internal interface IDelayedBackgroundTask : IBackgroundTask + { + /// + /// Gets a wait handle on the task condition. + /// + WaitHandle DelayWaitHandle { get; } + + /// + /// Gets a value indicating whether the task is delayed. + /// + bool IsDelayed { get; } + } +} diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs new file mode 100644 index 0000000000..91d86d97b4 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -0,0 +1,108 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Provides a base class for recurring background tasks. + /// + /// The type of the managed tasks. + internal abstract class RecurringTaskBase : IBackgroundTask + where T : class, IBackgroundTask + { + private readonly IBackgroundTaskRunner _runner; + private readonly int _periodMilliseconds; + private Timer _timer; + + /// + /// Initializes a new instance of the class with a tasks runner and a period. + /// + /// The task runner. + /// The period. + /// The task will repeat itself periodically. Use this constructor to create a new task. + protected RecurringTaskBase(IBackgroundTaskRunner runner, int periodMilliseconds) + { + _runner = runner; + _periodMilliseconds = periodMilliseconds; + } + + /// + /// Initializes a new instance of the class with a source task. + /// + /// The source task. + /// Use this constructor to create a new task from a source task in GetRecurring. + protected RecurringTaskBase(RecurringTaskBase source) + { + _runner = source._runner; + _periodMilliseconds = source._periodMilliseconds; + } + + /// + /// Implements IBackgroundTask.Run(). + /// + /// Classes inheriting from RecurringTaskBase must implement PerformRun. + public void Run() + { + PerformRun(); + Repeat(); + } + + /// + /// Implements IBackgroundTask.RunAsync(). + /// + /// Classes inheriting from RecurringTaskBase must implement PerformRun. + public async Task RunAsync() + { + await PerformRunAsync(); + Repeat(); + } + + private void Repeat() + { + // again? + if (_runner.IsCompleted) return; // fail fast + + if (_periodMilliseconds == 0) return; + + var recur = GetRecurring(); + if (recur == null) return; // done + + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _runner.TryAdd(recur); + }, null, _periodMilliseconds, 0); + } + + /// + /// Indicates whether the background task can run asynchronously. + /// + public abstract bool IsAsync { get; } + + /// + /// Runs the background task. + /// + public abstract void PerformRun(); + + /// + /// Runs the task asynchronously. + /// + /// A instance representing the execution of the background task. + public abstract Task PerformRunAsync(); + + /// + /// Gets a new occurence of the recurring task. + /// + /// A new task instance to be queued, or null to terminate the recurring task. + /// The new task instance must be created via the RecurringTaskBase(RecurringTaskBase{T} source) constructor, + /// where source is the current task, eg: return new MyTask(this); + protected abstract T GetRecurring(); + + /// + /// Dispose the task. + /// + public virtual void Dispose() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs b/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs new file mode 100644 index 0000000000..a57ea904b0 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/TaskAndFactoryExtensions.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + internal static class TaskAndFactoryExtensions + { + #region Task Extensions + + static void SetCompletionSource(TaskCompletionSource completionSource, Task task) + { + if (task.IsFaulted) + completionSource.SetException(task.Exception.InnerException); + else + completionSource.SetResult(default(TResult)); + } + + static void SetCompletionSource(TaskCompletionSource completionSource, Task task) + { + if (task.IsFaulted) + completionSource.SetException(task.Exception.InnerException); + else + completionSource.SetResult(task.Result); + } + + public static Task ContinueWithTask(this Task task, Func continuation) + { + var completionSource = new TaskCompletionSource(); + task.ContinueWith(atask => continuation(atask).ContinueWith(atask2 => SetCompletionSource(completionSource, atask2))); + return completionSource.Task; + } + + public static Task ContinueWithTask(this Task task, Func continuation, CancellationToken token) + { + var completionSource = new TaskCompletionSource(); + task.ContinueWith(atask => continuation(atask).ContinueWith(atask2 => SetCompletionSource(completionSource, atask2), token), token); + return completionSource.Task; + } + + #endregion + + #region TaskFactory Extensions + + public static Task Completed(this TaskFactory factory) + { + var taskSource = new TaskCompletionSource(); + taskSource.SetResult(null); + return taskSource.Task; + } + + public static Task Sync(this TaskFactory factory, Action action) + { + var taskSource = new TaskCompletionSource(); + action(); + taskSource.SetResult(null); + return taskSource.Task; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 23ba69cf31..58821bec6b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -499,6 +499,11 @@ + + + + + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index cd736b98ff..cbf296115a 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -33,7 +33,7 @@ namespace umbraco public class content { private static readonly BackgroundTaskRunner FilePersister - = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions {DedicatedThread = true, OnlyProcessLastItem = true}); + = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true }); #region Declarations From a73b7a584939785cb08efb28d4f7c2b2d16d0f3c Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 6 Feb 2015 18:10:19 +0100 Subject: [PATCH 113/249] refactor Scheduler to use new BackgroundTaskRunner capabilities Conflicts: src/Umbraco.Web/Scheduling/Scheduler.cs --- .../Scheduling/DelayedRecurringTaskBase.cs | 8 +- src/Umbraco.Web/Scheduling/LogScrubber.cs | 42 +++++-- .../Scheduling/RecurringTaskBase.cs | 7 +- .../Scheduling/ScheduledPublishing.cs | 33 ++++-- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 35 ++++-- src/Umbraco.Web/Scheduling/Scheduler.cs | 106 ++++-------------- 6 files changed, 113 insertions(+), 118 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index 573adeda3d..cac68241f4 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -34,12 +34,18 @@ namespace Umbraco.Web.Scheduling if (_gate != null) return _gate; _gate = new ManualResetEvent(false); + + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + _timer = new Timer(_ => { _timer.Dispose(); _timer = null; _gate.Set(); - }, null, _delayMilliseconds, 0); + }); + _timer.Change(_delayMilliseconds, 0); return _gate; } } diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 2edbe80726..a0c2c6979e 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -9,17 +9,31 @@ using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - internal class LogScrubber : DisposableObject, IBackgroundTask + internal class LogScrubber : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(ApplicationContext appContext, IUmbracoSettingsSection settings) + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } + public LogScrubber(LogScrubber source) + : base(source) + { + _appContext = source._appContext; + _settings = source._settings; + } + + protected override LogScrubber GetRecurring() + { + return new LogScrubber(this); + } + private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { int maximumAge = 24 * 60 * 60; @@ -36,14 +50,22 @@ namespace Umbraco.Web.Scheduling } - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { + public static int GetLogScrubbingInterval(IUmbracoSettingsSection settings) + { + int interval = 24 * 60 * 60; //24 hours + try + { + if (settings.Logging.CleaningMiliseconds > -1) + interval = settings.Logging.CleaningMiliseconds; + } + catch (Exception e) + { + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + } + return interval; } - public void Run() + public override void PerformRun() { using (DisposableTimer.DebugDuration(() => "Log scrubbing executing", () => "Log scrubbing complete")) { @@ -51,12 +73,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 91d86d97b4..553e62d3a0 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -67,12 +67,17 @@ namespace Umbraco.Web.Scheduling var recur = GetRecurring(); if (recur == null) return; // done + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + _timer = new Timer(_ => { _timer.Dispose(); _timer = null; _runner.TryAdd(recur); - }, null, _periodMilliseconds, 0); + }); + _timer.Change(_periodMilliseconds, 0); } /// diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index cacb4e133d..de92374379 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -12,30 +12,41 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing : DisposableObject, IBackgroundTask + internal class ScheduledPublishing : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private static bool _isPublishingRunning = false; + private static bool _isPublishingRunning; - public ScheduledPublishing(ApplicationContext appContext, IUmbracoSettingsSection settings) + public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } - - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() + private ScheduledPublishing(ScheduledPublishing source) + : base(source) { + _appContext = source._appContext; + _settings = source._settings; } - public void Run() + protected override ScheduledPublishing GetRecurring() + { + return new ScheduledPublishing(this); + } + + public override void PerformRun() { if (_appContext == null) return; + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return; + } using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) { @@ -77,12 +88,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index ddcb9ea533..bd3a3524f6 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -17,19 +17,33 @@ namespace Umbraco.Web.Scheduling // would need to be a publicly available task (URL) which isn't really very good :( // We should really be using the AdminTokenAuthorizeAttribute for this stuff - internal class ScheduledTasks : DisposableObject, IBackgroundTask + internal class ScheduledTasks : DelayedRecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); private static bool _isPublishingRunning = false; - public ScheduledTasks(ApplicationContext appContext, IUmbracoSettingsSection settings) + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext, IUmbracoSettingsSection settings) + : base(runner, delayMilliseconds, periodMilliseconds) { _appContext = appContext; _settings = settings; } + public ScheduledTasks(ScheduledTasks source) + : base(source) + { + _appContext = source._appContext; + _settings = source._settings; + } + + protected override ScheduledTasks GetRecurring() + { + return new ScheduledTasks(this); + } + private void ProcessTasks() { var scheduledTasks = _settings.ScheduledTasks.Tasks; @@ -78,15 +92,14 @@ namespace Umbraco.Web.Scheduling return false; } - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() + public override void PerformRun() { - } + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return; + } - public void Run() - { using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { if (_isPublishingRunning) return; @@ -108,12 +121,12 @@ namespace Umbraco.Web.Scheduling } } - public Task RunAsync() + public override Task PerformRunAsync() { throw new NotImplementedException(); } - public bool IsAsync + public override bool IsAsync { get { return false; } } diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index ee02947e20..6e586efad8 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -19,12 +19,10 @@ namespace Umbraco.Web.Scheduling internal sealed class Scheduler : ApplicationEventHandler { private static Timer _pingTimer; - private static Timer _schedulingTimer; - private static BackgroundTaskRunner _publishingRunner; - private static BackgroundTaskRunner _tasksRunner; - private static BackgroundTaskRunner _scrubberRunner; - private static Timer _logScrubberTimer; - private static volatile bool _started = false; + private static BackgroundTaskRunner _publishingRunner; + private static BackgroundTaskRunner _tasksRunner; + private static BackgroundTaskRunner _scrubberRunner; + private static volatile bool _started; private static readonly object Locker = new object(); protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) @@ -49,97 +47,37 @@ namespace Umbraco.Web.Scheduling _started = true; LogHelper.Debug(() => "Initializing the scheduler"); - // time to setup the tasks + // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _publishingRunner = new BackgroundTaskRunner(); + _tasksRunner = new BackgroundTaskRunner(); + _scrubberRunner = new BackgroundTaskRunner(); - //We have 3 background runners that are web aware, if the app domain dies, these tasks will wind down correctly - _publishingRunner = new BackgroundTaskRunner(); - _tasksRunner = new BackgroundTaskRunner(); - _scrubberRunner = new BackgroundTaskRunner(); + var settings = UmbracoConfig.For.UmbracoSettings(); - //NOTE: It is important to note that we need to use the ctor for a timer without the 'state' object specified, this is in order - // to ensure that the timer itself is not GC'd since internally .net will pass itself in as the state object and that will keep it alive. - // There's references to this here: http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - // we also make these timers static to ensure further GC safety. + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // also make the timer static to ensure further GC safety + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - // ping/keepalive - NOTE: we don't use a background runner for this because it does not need to be web aware, if the app domain dies, no problem + // ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); _pingTimer.Change(60000, 300000); // scheduled publishing/unpublishing - _schedulingTimer = new Timer(state => PerformScheduling(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _schedulingTimer.Change(60000, 60000); + // install on all, will only run on non-slaves servers + // both are delayed recurring tasks + _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); + _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); - //log scrubbing - _logScrubberTimer = new Timer(state => PerformLogScrub(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _logScrubberTimer.Change(60000, GetLogScrubbingInterval(UmbracoConfig.For.UmbracoSettings())); + // log scrubbing + // install & run on all servers + // LogScrubber is a delayed recurring task + _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); } } } }; }; } - - - private int GetLogScrubbingInterval(IUmbracoSettingsSection settings) - { - var interval = 4 * 60 * 60 * 1000; // 4 hours, in milliseconds - try - { - if (settings.Logging.CleaningMiliseconds > -1) - interval = settings.Logging.CleaningMiliseconds; - } - catch (Exception e) - { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 4 hours.", e); - } - return interval; - } - - private static void PerformLogScrub(ApplicationContext appContext, IUmbracoSettingsSection settings) - { - _scrubberRunner.Add(new LogScrubber(appContext, settings)); - } - - /// - /// This performs all of the scheduling on the one timer - /// - /// - /// - /// - /// No processing will be done if this server is a slave - /// - private static void PerformScheduling(ApplicationContext appContext, IUmbracoSettingsSection settings) - { - using (DisposableTimer.DebugDuration(() => "Scheduling interval executing", () => "Scheduling interval complete")) - { - //get the current server status to see if this server should execute the scheduled publishing - var serverStatus = ServerEnvironmentHelper.GetStatus(settings); - - switch (serverStatus) - { - case CurrentServerEnvironmentStatus.Single: - case CurrentServerEnvironmentStatus.Master: - case CurrentServerEnvironmentStatus.Unknown: - //if it's a single server install, a master or it cannot be determined - // then we will process the scheduling - - //do the scheduled publishing - _publishingRunner.Add(new ScheduledPublishing(appContext, settings)); - - //do the scheduled tasks - _tasksRunner.Add(new ScheduledTasks(appContext, settings)); - - break; - case CurrentServerEnvironmentStatus.Slave: - //do not process - - LogHelper.Debug( - () => string.Format("Current server ({0}) detected as a slave, no scheduled processes will execute on this server", NetworkHelper.MachineName)); - - break; - } - } - } - } } From be3702658711df34f88029630a390e80c111478f Mon Sep 17 00:00:00 2001 From: Stephan Date: Sun, 8 Feb 2015 16:25:30 +0100 Subject: [PATCH 114/249] refactor latched background tasks, now use a task for xml Conflicts: src/Umbraco.Web.UI/config/ClientDependency.config --- .../Scheduling/BackgroundTaskRunnerTests.cs | 11 +- .../XmlCacheFilePersister.cs | 115 ++++++++++++++++-- .../Scheduling/BackgroundTaskRunner.cs | 20 +-- .../Scheduling/DelayedRecurringTaskBase.cs | 66 ++++++---- .../Scheduling/IDelayedBackgroundTask.cs | 23 ---- .../Scheduling/ILatchedBackgroundTask.cs | 31 +++++ src/Umbraco.Web/Scheduling/LogScrubber.cs | 5 + .../Scheduling/RecurringTaskBase.cs | 22 ++-- .../Scheduling/ScheduledPublishing.cs | 5 + src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 5 + src/Umbraco.Web/Umbraco.Web.csproj | 2 +- .../umbraco.presentation/content.cs | 17 ++- 12 files changed, 239 insertions(+), 83 deletions(-) delete mode 100644 src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs create mode 100644 src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index e83ce400d9..299c11881d 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -564,7 +564,7 @@ namespace Umbraco.Tests.Scheduling } } - private class MyDelayedTask : IDelayedBackgroundTask + private class MyDelayedTask : ILatchedBackgroundTask { private readonly int _runMilliseconds; private readonly ManualResetEvent _gate; @@ -577,12 +577,17 @@ namespace Umbraco.Tests.Scheduling _gate = new ManualResetEvent(false); } - public WaitHandle DelayWaitHandle + public WaitHandle Latch { get { return _gate; } } - public bool IsDelayed + public bool IsLatched + { + get { return true; } + } + + public bool RunsOnShutdown { get { return true; } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 482a666538..1bed36160f 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using System.Xml; +using umbraco; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Scheduling; @@ -19,22 +20,120 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : DisposableObject, IBackgroundTask + internal class XmlCacheFilePersister : ILatchedBackgroundTask { - private readonly XmlDocument _xDoc; + private readonly IBackgroundTaskRunner _runner; private readonly string _xmlFileName; private readonly ProfilingLogger _logger; + private readonly content _content; + private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); + private readonly object _locko = new object(); + private bool _released; + private Timer _timer; + private DateTime _initialTouch; - public XmlCacheFilePersister(XmlDocument xDoc, string xmlFileName, ProfilingLogger logger) + private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min) + private const int MaxWaitMilliseconds = 10000; // save the cache after some time (ie no more than 10s of changes) + + // save the cache when the app goes down + public bool RunsOnShutdown { get { return true; } } + + public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, string xmlFileName, ProfilingLogger logger, bool touched = false) { - _xDoc = xDoc; + _runner = runner; + _content = content; _xmlFileName = xmlFileName; _logger = logger; + + if (touched == false) return; + + LogHelper.Debug("Create new touched, start."); + + _initialTouch = DateTime.Now; + _timer = new Timer(_ => Release()); + + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); } + public XmlCacheFilePersister Touch() + { + lock (_locko) + { + if (_released) + { + LogHelper.Debug("Touched, was released, create new."); + + // released, has run or is running, too late, add & return a new task + var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, _logger, true); + _runner.Add(persister); + return persister; + } + + if (_timer == null) + { + LogHelper.Debug("Touched, was idle, start."); + + // not started yet, start + _initialTouch = DateTime.Now; + _timer = new Timer(_ => Release()); + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); + return this; + } + + // set the timer to trigger in WaitMilliseconds unless we've been touched first more + // than MaxWaitMilliseconds ago and then release now + + if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds)) + { + LogHelper.Debug("Touched, was waiting, wait.", () => WaitMilliseconds); + LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _timer.Change(WaitMilliseconds, 0); + } + else + { + LogHelper.Debug("Save now, release."); + ReleaseLocked(); + } + + return this; // still available + } + } + + private void Release() + { + lock (_locko) + { + ReleaseLocked(); + } + } + + private void ReleaseLocked() + { + LogHelper.Debug("Timer: save now, release."); + if (_timer != null) + _timer.Dispose(); + _timer = null; + _released = true; + _latch.Set(); + } + + public WaitHandle Latch + { + get { return _latch.WaitHandle; } + } + + public bool IsLatched + { + get { return true; } + } + public async Task RunAsync() { - await PersistXmlToFileAsync(_xDoc); + LogHelper.Debug("Run now."); + var doc = _content.XmlContentInternal; + await PersistXmlToFileAsync(doc); } public bool IsAsync @@ -91,14 +190,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - protected override void DisposeResources() - { - } + public void Dispose() + { } public void Run() { throw new NotImplementedException(); } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index c511a0af53..82fb601815 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Scheduling private readonly BackgroundTaskRunnerOptions _options; private readonly BlockingCollection _tasks = new BlockingCollection(); private readonly object _locker = new object(); - private readonly ManualResetEvent _completedEvent = new ManualResetEvent(false); + private readonly ManualResetEventSlim _completedEvent = new ManualResetEventSlim(false); private volatile bool _isRunning; // is running private volatile bool _isCompleted; // does not accept tasks anymore, may still be running @@ -304,13 +304,19 @@ namespace Umbraco.Web.Scheduling return; } - // wait for delayed task, supporting cancellation - var dbgTask = bgTask as IDelayedBackgroundTask; - if (dbgTask != null && dbgTask.IsDelayed) + // wait for latched task, supporting cancellation + var dbgTask = bgTask as ILatchedBackgroundTask; + if (dbgTask != null && dbgTask.IsLatched) { - WaitHandle.WaitAny(new[] { dbgTask.DelayWaitHandle, token.WaitHandle, _completedEvent }); + WaitHandle.WaitAny(new[] { dbgTask.Latch, token.WaitHandle, _completedEvent.WaitHandle }); if (TaskSourceCanceled(taskSource, token)) return; - // else run now, either because delay is ok or runner is completed + // else run now, either because latch ok or runner is completed + // still latched & not running on shutdown = stop here + if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) + { + TaskSourceCompleted(taskSource, token); + return; + } } // run the task as first task, or a continuation @@ -378,7 +384,7 @@ namespace Umbraco.Web.Scheduling OnTaskError(new TaskEventArgs(bgTask, e)); throw; } - Console.WriteLine("!1"); + OnTaskCompleted(new TaskEventArgs(bgTask)); } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index cac68241f4..3ecae089cc 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -1,5 +1,6 @@ using System; using System.Threading; +using System.Threading.Tasks; namespace Umbraco.Web.Scheduling { @@ -7,52 +8,67 @@ namespace Umbraco.Web.Scheduling /// Provides a base class for recurring background tasks. /// /// The type of the managed tasks. - internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, IDelayedBackgroundTask + internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, ILatchedBackgroundTask where T : class, IBackgroundTask { - private readonly int _delayMilliseconds; - private ManualResetEvent _gate; + private readonly ManualResetEventSlim _latch; private Timer _timer; protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) : base(runner, periodMilliseconds) { - _delayMilliseconds = delayMilliseconds; + if (delayMilliseconds > 0) + { + _latch = new ManualResetEventSlim(false); + _timer = new Timer(_ => + { + _timer.Dispose(); + _timer = null; + _latch.Set(); + }); + _timer.Change(delayMilliseconds, 0); + } } protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) : base(source) { - _delayMilliseconds = 0; + // no latch on recurring instances + _latch = null; } - public WaitHandle DelayWaitHandle + public override void Run() + { + if (_latch != null) + _latch.Dispose(); + base.Run(); + } + + public override async Task RunAsync() + { + if (_latch != null) + _latch.Dispose(); + await base.RunAsync(); + } + + public WaitHandle Latch { get { - if (_delayMilliseconds == 0) return new ManualResetEvent(true); - - if (_gate != null) return _gate; - _gate = new ManualResetEvent(false); - - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _gate.Set(); - }); - _timer.Change(_delayMilliseconds, 0); - return _gate; + if (_latch == null) + throw new InvalidOperationException("The task is not latched."); + return _latch.WaitHandle; } } - public bool IsDelayed + public bool IsLatched { - get { return _delayMilliseconds > 0; } + get { return _latch != null; } + } + + public virtual bool RunsOnShutdown + { + get { return true; } } } } diff --git a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs deleted file mode 100644 index 01f8a5e01a..0000000000 --- a/src/Umbraco.Web/Scheduling/IDelayedBackgroundTask.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Represents a delayed background task. - /// - /// Delayed background tasks can suspend their execution until - /// a condition is met. However if the tasks runner has to terminate, - /// delayed background tasks are executed immediately. - internal interface IDelayedBackgroundTask : IBackgroundTask - { - /// - /// Gets a wait handle on the task condition. - /// - WaitHandle DelayWaitHandle { get; } - - /// - /// Gets a value indicating whether the task is delayed. - /// - bool IsDelayed { get; } - } -} diff --git a/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs new file mode 100644 index 0000000000..0ad4d42bdf --- /dev/null +++ b/src/Umbraco.Web/Scheduling/ILatchedBackgroundTask.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; + +namespace Umbraco.Web.Scheduling +{ + /// + /// Represents a latched background task. + /// + /// Latched background tasks can suspend their execution until + /// a condition is met. However if the tasks runner has to terminate, + /// latched background tasks can be executed immediately, depending on + /// the value returned by RunsOnShutdown. + internal interface ILatchedBackgroundTask : IBackgroundTask + { + /// + /// Gets a wait handle on the task condition. + /// + /// The task is not latched. + WaitHandle Latch { get; } + + /// + /// Gets a value indicating whether the task is latched. + /// + bool IsLatched { get; } + + /// + /// Gets a value indicating whether the task can be executed immediately if the task runner has to terminate. + /// + bool RunsOnShutdown { get; } + } +} diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index a0c2c6979e..c4a5ce2c00 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -82,5 +82,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 553e62d3a0..6bae7406f9 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.Scheduling private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; private Timer _timer; + private T _recurrent; /// /// Initializes a new instance of the class with a tasks runner and a period. @@ -34,6 +35,7 @@ namespace Umbraco.Web.Scheduling protected RecurringTaskBase(RecurringTaskBase source) { _runner = source._runner; + _timer = source._timer; _periodMilliseconds = source._periodMilliseconds; } @@ -41,7 +43,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public void Run() + public virtual void Run() { PerformRun(); Repeat(); @@ -51,7 +53,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public async Task RunAsync() + public virtual async Task RunAsync() { await PerformRunAsync(); Repeat(); @@ -64,19 +66,19 @@ namespace Umbraco.Web.Scheduling if (_periodMilliseconds == 0) return; - var recur = GetRecurring(); - if (recur == null) return; // done + _recurrent = GetRecurring(); + if (_recurrent == null) + { + _timer.Dispose(); + _timer = null; + return; // done + } // note // must use the single-parameter constructor on Timer to avoid it from being GC'd // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _runner.TryAdd(recur); - }); + _timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent)); _timer.Change(_periodMilliseconds, 0); } diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index de92374379..9db21fba8a 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -97,5 +97,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index bd3a3524f6..cba3cb4fc8 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -130,5 +130,10 @@ namespace Umbraco.Web.Scheduling { get { return false; } } + + public override bool RunsOnShutdown + { + get { return false; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 58821bec6b..24d5058e31 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -501,7 +501,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index cbf296115a..fa2e93b0b1 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -35,6 +35,15 @@ namespace umbraco private static readonly BackgroundTaskRunner FilePersister = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions { LongRunning = true }); + private XmlCacheFilePersister _persisterTask; + + private content() + { + _persisterTask = new XmlCacheFilePersister(FilePersister, this, UmbracoXmlDiskCacheFileName, + new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler)); + FilePersister.Add(_persisterTask); + } + #region Declarations // Sync access to disk file @@ -131,7 +140,7 @@ namespace umbraco /// /// Before returning we always check to ensure that the xml is loaded /// - protected virtual XmlDocument XmlContentInternal + protected internal virtual XmlDocument XmlContentInternal { get { @@ -317,8 +326,7 @@ namespace umbraco // and clear the queue in case is this a web request, we don't want it reprocessing. if (UmbracoConfig.For.UmbracoSettings().Content.XmlCacheEnabled && UmbracoConfig.For.UmbracoSettings().Content.ContinouslyUpdateXmlDiskCache) { - FilePersister.Add(new XmlCacheFilePersister(xmlDoc, UmbracoXmlDiskCacheFileName , - new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); + QueueXmlForPersistence(); } } } @@ -1230,8 +1238,7 @@ order by umbracoNode.level, umbracoNode.sortOrder"; /// private void QueueXmlForPersistence() { - FilePersister.Add(new XmlCacheFilePersister(_xmlContent, UmbracoXmlDiskCacheFileName, - new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler))); + _persisterTask = _persisterTask.Touch(); } internal DateTime GetCacheFileUpdateTime() From 4c3de920c6ec65362a65af2dfa7a5e8a89aedb88 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Feb 2015 15:05:42 +0100 Subject: [PATCH 115/249] Removes the 'else' so that 'wait' is always checked. --- src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 82fb601815..2dfba25a48 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -231,12 +231,11 @@ namespace Umbraco.Web.Scheduling // truncate running queues _tokenSource.Cancel(false); // false is the default } - else - { - // tasks in the queue will be executed... - if (wait == false) return; - _runningTask.Wait(); // wait for whatever is running to end... - } + + // tasks in the queue will be executed... + if (wait == false) return; + _runningTask.Wait(); // wait for whatever is running to end... + } /// From 58ce04e26b8444214e64fa58feb5abb4849057fa Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 17 Feb 2015 15:09:29 +0100 Subject: [PATCH 116/249] cleanup --- src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs | 8 ++++---- .../XmlPublishedCache/XmlCacheFilePersister.cs | 4 ++-- src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs | 5 ++--- src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs | 4 ++-- src/Umbraco.Web/Scheduling/IBackgroundTask.cs | 4 +++- src/Umbraco.Web/Scheduling/RecurringTaskBase.cs | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index 299c11881d..ab83294496 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -514,7 +514,7 @@ namespace Umbraco.Tests.Scheduling throw new Exception("Task has thrown."); } - public async Task RunAsync() + public async Task RunAsync(CancellationToken token) { await Task.Delay(1000); throw new Exception("Task has thrown."); @@ -603,7 +603,7 @@ namespace Umbraco.Tests.Scheduling _gate.Set(); } - public Task RunAsync() + public Task RunAsync(CancellationToken token) { throw new NotImplementedException(); } @@ -690,10 +690,10 @@ namespace Umbraco.Tests.Scheduling Ended = DateTime.Now; } - public Task RunAsync() + public Task RunAsync(CancellationToken token) { throw new NotImplementedException(); - //return Task.Delay(500); // fixme + //return Task.Delay(500); } public bool IsAsync diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 1bed36160f..1254951d82 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -128,8 +128,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { get { return true; } } - - public async Task RunAsync() + + public async Task RunAsync(CancellationToken token) { LogHelper.Debug("Run now."); var doc = _content.XmlContentInternal; diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 2dfba25a48..3a5ace8af8 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -14,7 +13,7 @@ namespace Umbraco.Web.Scheduling /// /// The type of the managed tasks. /// The task runner is web-aware and will ensure that it shuts down correctly when the AppDomain - /// shuts down (ie is unloaded). FIXME WHAT DOES THAT MEAN? + /// shuts down (ie is unloaded). internal class BackgroundTaskRunner : IBackgroundTaskRunner where T : class, IBackgroundTask { @@ -373,7 +372,7 @@ namespace Umbraco.Web.Scheduling using (bgTask) // ensure it's disposed { if (bgTask.IsAsync) - await bgTask.RunAsync(); // fixme should pass the token along?! + await bgTask.RunAsync(token); else bgTask.Run(); } diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs index 3ecae089cc..f7cec0079b 100644 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs @@ -44,11 +44,11 @@ namespace Umbraco.Web.Scheduling base.Run(); } - public override async Task RunAsync() + public override async Task RunAsync(CancellationToken token) { if (_latch != null) _latch.Dispose(); - await base.RunAsync(); + await base.RunAsync(token); } public WaitHandle Latch diff --git a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs index 9be2512d01..4e646c0623 100644 --- a/src/Umbraco.Web/Scheduling/IBackgroundTask.cs +++ b/src/Umbraco.Web/Scheduling/IBackgroundTask.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Umbraco.Web.Scheduling @@ -16,9 +17,10 @@ namespace Umbraco.Web.Scheduling /// /// Runs the task asynchronously. /// + /// A cancellation token. /// A instance representing the execution of the background task. /// The background task cannot run asynchronously. - Task RunAsync(); + Task RunAsync(CancellationToken token); /// /// Indicates whether the background task can run asynchronously. diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index 6bae7406f9..d710a70e03 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -53,7 +53,7 @@ namespace Umbraco.Web.Scheduling /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual async Task RunAsync() + public virtual async Task RunAsync(CancellationToken token) { await PerformRunAsync(); Repeat(); From 4bdac1475ff496062f9e6d81354ea8c22ffdccc7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 14:52:38 +1100 Subject: [PATCH 117/249] fixes log scrub interval with ms with updated background tasks --- src/Umbraco.Web/Scheduling/LogScrubber.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index c4a5ce2c00..a9f70a612e 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Scheduling public static int GetLogScrubbingInterval(IUmbracoSettingsSection settings) { - int interval = 24 * 60 * 60; //24 hours + var interval = 4 * 60 * 60 * 1000; // 4 hours, in milliseconds try { if (settings.Logging.CleaningMiliseconds > -1) @@ -60,7 +60,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 24 horus", e); + LogHelper.Error("Unable to locate a log scrubbing interval. Defaulting to 4 hours.", e); } return interval; } From ec742d1f6690d9ae9629abb5d2d8287c64ccd604 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 14:56:12 +1100 Subject: [PATCH 118/249] removes the usages of ILogger and ProfilerLogger since this is not for 7.3.0, will need to re-fix for 7.3 branch. --- .../XmlPublishedCache/XmlCacheFilePersister.cs | 11 ++++++----- src/Umbraco.Web/umbraco.presentation/content.cs | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 1254951d82..d304e99f93 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { private readonly IBackgroundTaskRunner _runner; private readonly string _xmlFileName; - private readonly ProfilingLogger _logger; + //private readonly ProfilingLogger _logger; private readonly content _content; private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); private readonly object _locko = new object(); @@ -38,12 +38,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // save the cache when the app goes down public bool RunsOnShutdown { get { return true; } } - public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, string xmlFileName, ProfilingLogger logger, bool touched = false) + public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, string xmlFileName, /*ProfilingLogger logger, */bool touched = false) { _runner = runner; _content = content; _xmlFileName = xmlFileName; - _logger = logger; + //_logger = logger; if (touched == false) return; @@ -65,7 +65,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache LogHelper.Debug("Touched, was released, create new."); // released, has run or is running, too late, add & return a new task - var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, _logger, true); + var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, /*_logger, */true); _runner.Add(persister); return persister; } @@ -149,7 +149,8 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (xmlDoc != null) { - using (_logger.DebugDuration( + //using (_logger.DebugDuration( + using(DisposableTimer.DebugDuration( string.Format("Saving content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread), string.Format("Saved content to disk on thread '{0}' (Threadpool? {1})", Thread.CurrentThread.Name, Thread.CurrentThread.IsThreadPoolThread))) { diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index fa2e93b0b1..93e07a680c 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -39,8 +39,8 @@ namespace umbraco private content() { - _persisterTask = new XmlCacheFilePersister(FilePersister, this, UmbracoXmlDiskCacheFileName, - new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler)); + _persisterTask = new XmlCacheFilePersister(FilePersister, this, UmbracoXmlDiskCacheFileName + /*,new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler)*/); FilePersister.Add(_persisterTask); } From fda72b2acc1d2d93f5d11acebd3b94f9f62005dc Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 15:19:31 +1100 Subject: [PATCH 119/249] changes the MaxWaitMilliseconds tp 30000 to be consistent with the 7.3 branch --- .../PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index d304e99f93..9fd58eedbc 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private DateTime _initialTouch; private const int WaitMilliseconds = 4000; // save the cache 4s after the last change (ie every 4s min) - private const int MaxWaitMilliseconds = 10000; // save the cache after some time (ie no more than 10s of changes) + private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down public bool RunsOnShutdown { get { return true; } } From 5464107a9af0d6a261455b632fecd46053f0f96a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 27 Mar 2015 15:21:12 +1100 Subject: [PATCH 120/249] replaces all LogHelper calls with to use ILogger in XmlCacheFilePersister --- .../XmlCacheFilePersister.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index f307ff030f..319bb874c1 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -47,12 +47,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (touched == false) return; - LogHelper.Debug("Create new touched, start."); + _logger.Logger.Debug("Create new touched, start."); _initialTouch = DateTime.Now; _timer = new Timer(_ => Release()); - LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _logger.Logger.Debug("Save in {0}ms.", () => WaitMilliseconds); _timer.Change(WaitMilliseconds, 0); } @@ -62,7 +62,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { if (_released) { - LogHelper.Debug("Touched, was released, create new."); + _logger.Logger.Debug("Touched, was released, create new."); // released, has run or is running, too late, add & return a new task var persister = new XmlCacheFilePersister(_runner, _content, _xmlFileName, _logger, true); @@ -72,12 +72,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (_timer == null) { - LogHelper.Debug("Touched, was idle, start."); + _logger.Logger.Debug("Touched, was idle, start."); // not started yet, start _initialTouch = DateTime.Now; _timer = new Timer(_ => Release()); - LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _logger.Logger.Debug("Save in {0}ms.", () => WaitMilliseconds); _timer.Change(WaitMilliseconds, 0); return this; } @@ -87,13 +87,13 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache if (DateTime.Now - _initialTouch < TimeSpan.FromMilliseconds(MaxWaitMilliseconds)) { - LogHelper.Debug("Touched, was waiting, wait.", () => WaitMilliseconds); - LogHelper.Debug("Save in {0}ms.", () => WaitMilliseconds); + _logger.Logger.Debug("Touched, was waiting, wait.", () => WaitMilliseconds); + _logger.Logger.Debug("Save in {0}ms.", () => WaitMilliseconds); _timer.Change(WaitMilliseconds, 0); } else { - LogHelper.Debug("Save now, release."); + _logger.Logger.Debug("Save now, release."); ReleaseLocked(); } @@ -111,7 +111,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private void ReleaseLocked() { - LogHelper.Debug("Timer: save now, release."); + _logger.Logger.Debug("Timer: save now, release."); if (_timer != null) _timer.Dispose(); _timer = null; @@ -131,7 +131,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache public async Task RunAsync(CancellationToken token) { - LogHelper.Debug("Run now."); + _logger.Logger.Debug("Run now."); var doc = _content.XmlContentInternal; await PersistXmlToFileAsync(doc); } @@ -167,7 +167,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // If for whatever reason something goes wrong here, invalidate disk cache DeleteXmlCache(); - LogHelper.Error("Error saving content to disk", ee); + _logger.Logger.Error("Error saving content to disk", ee); } } From 1c3a7b95fc5788c372732521629d59112a9c365e Mon Sep 17 00:00:00 2001 From: Magnus Kragelund Date: Fri, 27 Mar 2015 11:02:35 +0100 Subject: [PATCH 121/249] fixes bug when trying to render a deleted node in multinode tree picker --- .../contentpicker/contentpicker.controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 9d755d7d3f..9002535488 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -175,8 +175,10 @@ function contentPickerController($scope, dialogService, entityResource, editorSt return d.id == id; }); - entity.icon = iconHelper.convertFromLegacyIcon(entity.icon); - $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon }); + if(entity != undefined) { + entity.icon = iconHelper.convertFromLegacyIcon(entity.icon); + $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon }); + } }); From 7a31d6cae3898fbf04c07dd79e5de70015d546d3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 17:23:28 +1100 Subject: [PATCH 122/249] better exception logging for examine --- src/UmbracoExamine/UmbracoContentIndexer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index 26ae4a9f5f..3813e650e8 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -237,7 +237,7 @@ namespace UmbracoExamine protected override void OnIndexingError(IndexingErrorEventArgs e) { - DataService.LogService.AddErrorLog(e.NodeId, string.Format("{0},{1}, IndexSet: {2}", e.Message, e.InnerException != null ? e.InnerException.Message : "", this.IndexSetName)); + DataService.LogService.AddErrorLog(e.NodeId, string.Format("{0},{1}, IndexSet: {2}", e.Message, e.InnerException != null ? e.InnerException.ToString() : "", this.IndexSetName)); base.OnIndexingError(e); } From 58857c5d83997e26fdeb6990feb948e649fc0413 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 17:36:54 +1100 Subject: [PATCH 123/249] Moves publishing strategies into CacheRefresherEventHandler where the event handlers are supposed to exist. --- .../Cache/CacheRefresherEventHandler.cs | 81 +++++++++++++++++++ .../Publishing/UpdateCacheAfterPublish.cs | 66 +-------------- .../Publishing/UpdateCacheAfterUnPublish.cs | 44 +--------- 3 files changed, 85 insertions(+), 106 deletions(-) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 56c0669819..8655acd9a7 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -14,6 +14,7 @@ using umbraco.cms.businesslogic; using umbraco.cms.businesslogic.member; using System.Linq; using umbraco.cms.businesslogic.web; +using Umbraco.Core.Publishing; using Content = Umbraco.Core.Models.Content; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; using DeleteEventArgs = umbraco.cms.businesslogic.DeleteEventArgs; @@ -143,10 +144,90 @@ namespace Umbraco.Web.Cache ContentService.Trashed += ContentServiceTrashed; ContentService.EmptiedRecycleBin += ContentServiceEmptiedRecycleBin; + PublishingStrategy.Published += PublishingStrategy_Published; + PublishingStrategy.UnPublished += PublishingStrategy_UnPublished; + //public access events Access.AfterSave += Access_AfterSave; } + #region Publishing + + void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) + { + if (e.PublishedEntities.Any()) + { + if (e.PublishedEntities.Count() > 1) + { + foreach (var c in e.PublishedEntities) + { + UnPublishSingle(c); + } + } + else + { + var content = e.PublishedEntities.FirstOrDefault(); + UnPublishSingle(content); + } + } + } + + /// + /// Refreshes the xml cache for a single node by removing it + /// + private void UnPublishSingle(IContent content) + { + DistributedCache.Instance.RemovePageCache(content); + } + + void PublishingStrategy_Published(IPublishingStrategy sender, PublishEventArgs e) + { + if (e.PublishedEntities.Any()) + { + if (e.IsAllRepublished) + { + UpdateEntireCache(); + return; + } + + if (e.PublishedEntities.Count() > 1) + { + UpdateMultipleContentCache(e.PublishedEntities); + } + else + { + var content = e.PublishedEntities.FirstOrDefault(); + UpdateSingleContentCache(content); + } + } + } + + /// + /// Refreshes the xml cache for all nodes + /// + private void UpdateEntireCache() + { + DistributedCache.Instance.RefreshAllPageCache(); + } + + /// + /// Refreshes the xml cache for nodes in list + /// + private void UpdateMultipleContentCache(IEnumerable content) + { + DistributedCache.Instance.RefreshPageCache(content.ToArray()); + } + + /// + /// Refreshes the xml cache for a single node + /// + private void UpdateSingleContentCache(IContent content) + { + DistributedCache.Instance.RefreshPageCache(content); + } + + #endregion + #region Public access event handlers static void Access_AfterSave(Access sender, SaveEventArgs e) diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs index 1371c6fdb9..a5cab77bc8 100644 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs +++ b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Core.Events; @@ -8,69 +9,8 @@ using Umbraco.Web.Cache; namespace Umbraco.Web.Strategies.Publishing { - //TODO: I think we should move this logic into the CacheRefresherEventHandler since all other handlers are registered there for invalidating cache. - - /// - /// Represents the UpdateCacheAfterPublish class, which subscribes to the Published event - /// of the class and is responsible for doing the actual - /// cache refresh after a content item has been published. - /// - /// - /// This implementation is meant as a seperation of the cache refresh from the ContentService - /// and PublishingStrategy. - /// This event subscriber will only be relevant as long as there is an xml cache. - /// + [Obsolete("This is not used and will be removed from the codebase in future versions")] public class UpdateCacheAfterPublish : ApplicationEventHandler { - protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { - PublishingStrategy.Published += PublishingStrategy_Published; - } - - void PublishingStrategy_Published(IPublishingStrategy sender, PublishEventArgs e) - { - if (e.PublishedEntities.Any()) - { - if (e.IsAllRepublished) - { - UpdateEntireCache(); - return; - } - - if (e.PublishedEntities.Count() > 1) - { - UpdateMultipleContentCache(e.PublishedEntities); - } - else - { - var content = e.PublishedEntities.FirstOrDefault(); - UpdateSingleContentCache(content); - } - } - } - - /// - /// Refreshes the xml cache for all nodes - /// - private void UpdateEntireCache() - { - DistributedCache.Instance.RefreshAllPageCache(); - } - - /// - /// Refreshes the xml cache for nodes in list - /// - private void UpdateMultipleContentCache(IEnumerable content) - { - DistributedCache.Instance.RefreshPageCache(content.ToArray()); - } - - /// - /// Refreshes the xml cache for a single node - /// - private void UpdateSingleContentCache(IContent content) - { - DistributedCache.Instance.RefreshPageCache(content); - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs index 39ca0beda3..e49f78f6c8 100644 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs +++ b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs @@ -10,50 +10,8 @@ using Umbraco.Web.Cache; namespace Umbraco.Web.Strategies.Publishing { - //TODO: I think we should move this logic into the CacheRefresherEventHandler since all other handlers are registered there for invalidating cache. - - /// - /// Represents the UpdateCacheAfterUnPublish class, which subscribes to the UnPublished event - /// of the class and is responsible for doing the actual - /// cache refresh after a content item has been unpublished. - /// - /// - /// This implementation is meant as a seperation of the cache refresh from the ContentService - /// and PublishingStrategy. - /// This event subscriber will only be relevant as long as there is an xml cache. - /// + [Obsolete("This is not used and will be removed from the codebase in future versions")] public class UpdateCacheAfterUnPublish : ApplicationEventHandler { - protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) - { - PublishingStrategy.UnPublished += PublishingStrategy_UnPublished; - } - - void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) - { - if (e.PublishedEntities.Any()) - { - if (e.PublishedEntities.Count() > 1) - { - foreach (var c in e.PublishedEntities) - { - UnPublishSingle(c); - } - } - else - { - var content = e.PublishedEntities.FirstOrDefault(); - UnPublishSingle(content); - } - } - } - - /// - /// Refreshes the xml cache for a single node by removing it - /// - private void UnPublishSingle(IContent content) - { - DistributedCache.Instance.RemovePageCache(content); - } } } \ No newline at end of file From fb24becd0d46555697656076065cf058dc9f379c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 17:40:36 +1100 Subject: [PATCH 124/249] completes: U4-6468 Move all 'Strategies' in the Web project that performs Core data operations to the Core project --- .../Strategies/RelateOnCopyHandler.cs | 1 + ...acheAfterUnPublish.cs => LegacyClasses.cs} | 37 +++++++++++-------- .../Publishing/UpdateCacheAfterPublish.cs | 16 -------- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- 4 files changed, 23 insertions(+), 34 deletions(-) rename src/Umbraco.Web/Strategies/{Publishing/UpdateCacheAfterUnPublish.cs => LegacyClasses.cs} (71%) delete mode 100644 src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs diff --git a/src/Umbraco.Core/Strategies/RelateOnCopyHandler.cs b/src/Umbraco.Core/Strategies/RelateOnCopyHandler.cs index e8c1956f2d..8b29bb0da4 100644 --- a/src/Umbraco.Core/Strategies/RelateOnCopyHandler.cs +++ b/src/Umbraco.Core/Strategies/RelateOnCopyHandler.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Strategies { + //TODO: This should just exist in the content service/repo! public sealed class RelateOnCopyHandler : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs b/src/Umbraco.Web/Strategies/LegacyClasses.cs similarity index 71% rename from src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs rename to src/Umbraco.Web/Strategies/LegacyClasses.cs index e49f78f6c8..434ab8ccbf 100644 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterUnPublish.cs +++ b/src/Umbraco.Web/Strategies/LegacyClasses.cs @@ -1,17 +1,22 @@ -using System; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Events; -using Umbraco.Core.Models; -using Umbraco.Core.Publishing; -using Umbraco.Web.Cache; - - -namespace Umbraco.Web.Strategies.Publishing -{ - [Obsolete("This is not used and will be removed from the codebase in future versions")] - public class UpdateCacheAfterUnPublish : ApplicationEventHandler - { - } +using System; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Publishing; +using Umbraco.Web.Cache; + + +namespace Umbraco.Web.Strategies.Publishing +{ + [Obsolete("This is not used and will be removed from the codebase in future versions")] + public class UpdateCacheAfterPublish : ApplicationEventHandler + { + } + + [Obsolete("This is not used and will be removed from the codebase in future versions")] + public class UpdateCacheAfterUnPublish : ApplicationEventHandler + { + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs b/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs deleted file mode 100644 index a5cab77bc8..0000000000 --- a/src/Umbraco.Web/Strategies/Publishing/UpdateCacheAfterPublish.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Events; -using Umbraco.Core.Models; -using Umbraco.Core.Publishing; -using Umbraco.Web.Cache; - -namespace Umbraco.Web.Strategies.Publishing -{ - [Obsolete("This is not used and will be removed from the codebase in future versions")] - public class UpdateCacheAfterPublish : ApplicationEventHandler - { - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 24d5058e31..2d9077bdc8 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -897,8 +897,7 @@ - - + From 76a2f433d92a23294d03ae26d5c8e97fe1efc493 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 18:42:47 +1100 Subject: [PATCH 125/249] simplifies build since we don't have the examine azure stuff --- build/Build.proj | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/build/Build.proj b/build/Build.proj index 40e24b6eb1..d65d50a0e3 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -90,8 +90,6 @@ $(BuildFolderRelativeToProjects)WebPi\ $(BuildFolderAbsolutePath)WebPi\ $(BuildFolderAbsolutePath)UmbracoExamine.PDF\ - $(BuildFolderAbsolutePath)UmbracoExamine.Azure\ - $(BuildFolderAbsolutePath)UmbracoExamine.PDF.Azure\ @@ -283,34 +281,22 @@ - + - + - - - + - - - - - - + SkipUnchangedFiles="false" /> From e0effec3b463d2c3024c525c410caaca4db10f40 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 19:13:13 +1100 Subject: [PATCH 126/249] fixes pdf build to only have it's required files in the zip --- build/Build.proj | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/build/Build.proj b/build/Build.proj index d65d50a0e3..af7be44c7c 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -75,7 +75,7 @@ UmbracoCms$(DECIMAL_BUILD_NUMBER).zip UmbracoCms.AllBinaries$(DECIMAL_BUILD_NUMBER).zip UmbracoCms.WebPI$(DECIMAL_BUILD_NUMBER).zip - UmbracoExamine.PDF.0.6.0.zip + UmbracoExamine.PDF.0.7.0.zip False ..\..\build\$(BuildFolder) $(MSBuildProjectDirectory)\$(BuildFolder) @@ -296,7 +296,13 @@ + SkipUnchangedFiles="false" /> + + + + + + From f6b9fdba725f9df29a334a505d8479794053de33 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 20:06:51 +1100 Subject: [PATCH 127/249] Removes old docs folder --- docs/License.txt | 13 ------------- docs/README.txt | 33 --------------------------------- 2 files changed, 46 deletions(-) delete mode 100644 docs/License.txt delete mode 100644 docs/README.txt diff --git a/docs/License.txt b/docs/License.txt deleted file mode 100644 index 1db5dd3f73..0000000000 --- a/docs/License.txt +++ /dev/null @@ -1,13 +0,0 @@ -Umbraco CMS version 4.5 is licensed under the OSI approved MIT License: - -Copyright (c) 2007-2010 umbraco I/S. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Umbraco versions prior to 4.5 were licensed under a combination of MIT and a propriatary UI license. The license history can be found here: -http://umbraco.codeplex.com/license \ No newline at end of file diff --git a/docs/README.txt b/docs/README.txt deleted file mode 100644 index 7321809543..0000000000 --- a/docs/README.txt +++ /dev/null @@ -1,33 +0,0 @@ -Umbraco - the simple, flexible and friendly ASP.NET CMS -======================================================= -For the first time on the Microsoft platform a free user and developer friendly cms that makes it quick and easy to create websites - or a breeze to build complex web applications. umbraco got award-winning integration capabilities and supports your ASP.NET User and Custom Controls out of the box. It's a developers dream and your users will love it too. Used by more than 57.000 active websites including Heinz.com, Peugeot.com, NAIAS.com and Microsofts documentinteropinitiative.org website you can be sure that the technology is proven, stable and scales. - -More info at http://umbraco.com - -Exploring the repository -======================== -Most contributors will work on their own fork and should pick the branch in which they need their fix. For more information see: -http://our.umbraco.org/contribute/guidelines-for-core-contribution - -Exploring the source -==================== -The Umbraco source code is never required for you to start building sites. If you are not using the source code for debugging purposes or to contribute to the source then please download the binaries listed at http://umbraco.codeplex.com - -With that said, the Umbraco solution can be opened in Visual Studio 2010 with SP1 installed or Visual Studio 2012. - -Contributing -============ -Umbraco is Open Source which means you can contribute to make it great! -Read all about it at http://our.umbraco.org/contribute - -Forums -====== -We have a forum running on http://our.umbraco.org for friendly community support. - -Documentation -============= -Our documentation section provides help on installing and using Umbraco including API reference documentation: http://our.umbraco.org/documentation - -Submitting Issues -================= -Another way you can contribute to Umbraco is by providing issue reports, for information on how to submit an issue report refer to our online guide at http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature \ No newline at end of file From cc8962f090ac499d510c4f0d65d1b83b2f63b096 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 20:07:26 +1100 Subject: [PATCH 128/249] Removes the old umbracoexamine.azure libs --- .../AzureContentIndexer.cs | 82 -------------- .../AzureMemberIndexer.cs | 78 ------------- src/UmbracoExamine.Azure/AzureSearcher.cs | 57 ---------- .../Properties/AssemblyInfo.cs | 29 ----- .../UmbracoAzureDataService.cs | 13 --- .../UmbracoAzureLogService.cs | 30 ----- .../UmbracoExamine.Azure.csproj | 103 ----------------- src/UmbracoExamine.Azure/app.config | 19 ---- src/UmbracoExamine.Azure/license.txt | 5 - src/UmbracoExamine.Azure/packages.config | 10 -- .../AzurePDFIndexer.cs | 84 -------------- .../Properties/AssemblyInfo.cs | 25 ----- .../UmbracoExamine.PDF.Azure.csproj | 104 ------------------ src/UmbracoExamine.PDF.Azure/app.config | 19 ---- src/UmbracoExamine.PDF.Azure/packages.config | 10 -- src/UmbracoExamine.PDF.Azure/packages.old | 6 - 16 files changed, 674 deletions(-) delete mode 100644 src/UmbracoExamine.Azure/AzureContentIndexer.cs delete mode 100644 src/UmbracoExamine.Azure/AzureMemberIndexer.cs delete mode 100644 src/UmbracoExamine.Azure/AzureSearcher.cs delete mode 100644 src/UmbracoExamine.Azure/Properties/AssemblyInfo.cs delete mode 100644 src/UmbracoExamine.Azure/UmbracoAzureDataService.cs delete mode 100644 src/UmbracoExamine.Azure/UmbracoAzureLogService.cs delete mode 100644 src/UmbracoExamine.Azure/UmbracoExamine.Azure.csproj delete mode 100644 src/UmbracoExamine.Azure/app.config delete mode 100644 src/UmbracoExamine.Azure/license.txt delete mode 100644 src/UmbracoExamine.Azure/packages.config delete mode 100644 src/UmbracoExamine.PDF.Azure/AzurePDFIndexer.cs delete mode 100644 src/UmbracoExamine.PDF.Azure/Properties/AssemblyInfo.cs delete mode 100644 src/UmbracoExamine.PDF.Azure/UmbracoExamine.PDF.Azure.csproj delete mode 100644 src/UmbracoExamine.PDF.Azure/app.config delete mode 100644 src/UmbracoExamine.PDF.Azure/packages.config delete mode 100644 src/UmbracoExamine.PDF.Azure/packages.old diff --git a/src/UmbracoExamine.Azure/AzureContentIndexer.cs b/src/UmbracoExamine.Azure/AzureContentIndexer.cs deleted file mode 100644 index 7f8a124121..0000000000 --- a/src/UmbracoExamine.Azure/AzureContentIndexer.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Examine; -using Examine.Azure; -using Examine.LuceneEngine.Config; -using Lucene.Net.Analysis; -using Lucene.Net.QueryParsers; -using Lucene.Net.Store.Azure; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.ServiceRuntime; -using UmbracoExamine.DataServices; - -namespace UmbracoExamine.Azure -{ - public class AzureContentIndexer : UmbracoContentIndexer, IAzureCatalogue - { - /// - /// static constructor run to initialize azure settings - /// - static AzureContentIndexer() - { - AzureExtensions.EnsureAzureConfig(); - } - - /// - /// Default constructor - /// - public AzureContentIndexer() - : base() - { - //By default, we will be using the UmbracoAzureDataService - DataService = new UmbracoAzureDataService(); - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - /// - public AzureContentIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - - } - - public string Catalogue { get; private set; } - - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - base.Initialize(name, config); - - this.SetOptimizationThresholdOnInit(config); - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - Catalogue = indexSet.IndexPath; - - } - - private Lucene.Net.Store.Directory _directory; - public override Lucene.Net.Store.Directory GetLuceneDirectory() - { - return _directory ?? (_directory = this.GetAzureDirectory()); - } - - public override Lucene.Net.Index.IndexWriter GetIndexWriter() - { - return this.GetAzureIndexWriter(); - } - - protected override void OnIndexingError(IndexingErrorEventArgs e) - { - AzureExtensions.LogExceptionFile(Name, e); - base.OnIndexingError(e); - } - - } -} diff --git a/src/UmbracoExamine.Azure/AzureMemberIndexer.cs b/src/UmbracoExamine.Azure/AzureMemberIndexer.cs deleted file mode 100644 index a10cd28886..0000000000 --- a/src/UmbracoExamine.Azure/AzureMemberIndexer.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.IO; -using Examine; -using Examine.Azure; -using Examine.LuceneEngine.Config; -using Lucene.Net.Analysis; -using Lucene.Net.QueryParsers; -using Lucene.Net.Store.Azure; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.ServiceRuntime; -using UmbracoExamine.DataServices; - -namespace UmbracoExamine.Azure -{ - public class AzureMemberIndexer : UmbracoMemberIndexer, IAzureCatalogue - { - /// - /// static constructor run to initialize azure settings - /// - static AzureMemberIndexer() - { - AzureExtensions.EnsureAzureConfig(); - } - - /// - /// Default constructor - /// - public AzureMemberIndexer() - : base() - { - //By default, we will be using the UmbracoAzureDataService - DataService = new UmbracoAzureDataService(); - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - /// - public AzureMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexerData, indexPath, dataService, analyzer, async) - { - - } - - public string Catalogue { get; private set; } - - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - base.Initialize(name, config); - - this.SetOptimizationThresholdOnInit(config); - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - Catalogue = indexSet.IndexPath; - - } - - private Lucene.Net.Store.Directory _directory; - public override Lucene.Net.Store.Directory GetLuceneDirectory() - { - return _directory ?? (_directory = this.GetAzureDirectory()); - } - - public override Lucene.Net.Index.IndexWriter GetIndexWriter() - { - return this.GetAzureIndexWriter(); - } - - protected override void OnIndexingError(IndexingErrorEventArgs e) - { - AzureExtensions.LogExceptionFile(Name, e); - base.OnIndexingError(e); - } - } -} \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/AzureSearcher.cs b/src/UmbracoExamine.Azure/AzureSearcher.cs deleted file mode 100644 index 87ebfc5aa1..0000000000 --- a/src/UmbracoExamine.Azure/AzureSearcher.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.IO; -using Examine.Azure; -using Examine.LuceneEngine.Config; -using Lucene.Net.Analysis; -using Lucene.Net.Store.Azure; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.ServiceRuntime; - -namespace UmbracoExamine.Azure -{ - - - public class AzureSearcher : UmbracoExamineSearcher, IAzureCatalogue - { - /// - /// static constructor run to initialize azure settings - /// - static AzureSearcher() - { - AzureExtensions.EnsureAzureConfig(); - } - - /// - /// Default constructor - /// - public AzureSearcher() - { - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - public AzureSearcher(DirectoryInfo indexPath, Analyzer analyzer) - : base(indexPath, analyzer) - { - } - - - public string Catalogue { get; private set; } - - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - base.Initialize(name, config); - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - Catalogue = indexSet.IndexPath; - } - - protected override Lucene.Net.Store.Directory GetLuceneDirectory() - { - return this.GetAzureDirectory(); - } - - } -} \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/Properties/AssemblyInfo.cs b/src/UmbracoExamine.Azure/Properties/AssemblyInfo.cs deleted file mode 100644 index 1c600b30f8..0000000000 --- a/src/UmbracoExamine.Azure/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyCompany("umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: AssemblyTitle("UmbracoExamine.Azure")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("UmbracoExamine.Azure")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//NOTE: WE cannot make change the major version to be the same as Umbraco because of backwards compatibility, however we -// will make the minor version the same as the umbraco version -[assembly: AssemblyVersion("0.6.0.*")] -[assembly: AssemblyFileVersion("0.6.0.*")] - - - -[assembly: InternalsVisibleTo("Umbraco.Tests")] \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/UmbracoAzureDataService.cs b/src/UmbracoExamine.Azure/UmbracoAzureDataService.cs deleted file mode 100644 index 2b8da60771..0000000000 --- a/src/UmbracoExamine.Azure/UmbracoAzureDataService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using UmbracoExamine.DataServices; - -namespace UmbracoExamine.Azure -{ - public class UmbracoAzureDataService : UmbracoDataService - { - public UmbracoAzureDataService() - { - //overwrite the log service - LogService = new UmbracoAzureLogService(); - } - } -} \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/UmbracoAzureLogService.cs b/src/UmbracoExamine.Azure/UmbracoAzureLogService.cs deleted file mode 100644 index 9abb88353a..0000000000 --- a/src/UmbracoExamine.Azure/UmbracoAzureLogService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Examine; -using Examine.Azure; -using UmbracoExamine.DataServices; - -namespace UmbracoExamine.Azure -{ - public class UmbracoAzureLogService : ILogService - { - public string ProviderName { get; set; } - - public void AddErrorLog(int nodeId, string msg) - { - AzureExtensions.LogExceptionFile(ProviderName, new IndexingErrorEventArgs(msg, nodeId, null)); - } - - public void AddInfoLog(int nodeId, string msg) - { - if (LogLevel == LoggingLevel.Verbose) - AzureExtensions.LogMessageFile("[UmbracoExamine] (" + ProviderName + ")" + msg + ". " + nodeId); - } - - public void AddVerboseLog(int nodeId, string msg) - { - if (LogLevel == LoggingLevel.Verbose) - AzureExtensions.LogMessageFile("[UmbracoExamine] (" + ProviderName + ")" + msg + ". " + nodeId); - } - - public LoggingLevel LogLevel { get; set; } - } -} \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/UmbracoExamine.Azure.csproj b/src/UmbracoExamine.Azure/UmbracoExamine.Azure.csproj deleted file mode 100644 index 6ed87b9cc4..0000000000 --- a/src/UmbracoExamine.Azure/UmbracoExamine.Azure.csproj +++ /dev/null @@ -1,103 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {73529637-28F5-419C-A6BB-D094E39DE614} - Library - Properties - UmbracoExamine.Azure - UmbracoExamine.Azure - v4.5 - 512 - ..\ - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - SecurityRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\AzureDirectory.1.0.5\lib\AzureDirectory.dll - - - False - ..\packages\Examine.0.1.57.2941\lib\Examine.dll - - - False - ..\packages\Examine.Azure.0.1.51.2941\lib\Examine.Azure.dll - - - ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - - ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - - ..\packages\AnglicanGeek.WindowsAzure.ServiceRuntime.1.6\lib\net35-full\Microsoft.WindowsAzure.ServiceRuntime.dll - - - ..\packages\WindowsAzure.Storage.1.6\lib\net35-full\Microsoft.WindowsAzure.StorageClient.dll - - - - - - - - - - - - - - - - - - - - - - {07FBC26B-2927-4A22-8D96-D644C667FECC} - UmbracoExamine - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/app.config b/src/UmbracoExamine.Azure/app.config deleted file mode 100644 index b77bae14a4..0000000000 --- a/src/UmbracoExamine.Azure/app.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/license.txt b/src/UmbracoExamine.Azure/license.txt deleted file mode 100644 index b89690ca4f..0000000000 --- a/src/UmbracoExamine.Azure/license.txt +++ /dev/null @@ -1,5 +0,0 @@ -Copyright 2011 Shannon Deminick Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy of the License -at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, -software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/src/UmbracoExamine.Azure/packages.config b/src/UmbracoExamine.Azure/packages.config deleted file mode 100644 index 0d4b103fae..0000000000 --- a/src/UmbracoExamine.Azure/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF.Azure/AzurePDFIndexer.cs b/src/UmbracoExamine.PDF.Azure/AzurePDFIndexer.cs deleted file mode 100644 index 33a19a1a7f..0000000000 --- a/src/UmbracoExamine.PDF.Azure/AzurePDFIndexer.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Examine; -using Examine.Azure; -using Examine.LuceneEngine.Config; -using Lucene.Net.Analysis; -using Lucene.Net.QueryParsers; -using Lucene.Net.Store.Azure; -using Microsoft.WindowsAzure; -using Microsoft.WindowsAzure.ServiceRuntime; -using UmbracoExamine.Azure; -using UmbracoExamine.DataServices; - -namespace UmbracoExamine.PDF.Azure -{ - public class AzurePDFIndexer : PDFIndexer, IAzureCatalogue - { - /// - /// static constructor run to initialize azure settings - /// - static AzurePDFIndexer() - { - AzureExtensions.EnsureAzureConfig(); - } - - /// - /// Default constructor - /// - public AzurePDFIndexer() - { - SupportedExtensions = new[] { ".pdf" }; - UmbracoFileProperty = "umbracoFile"; - //By default, we will be using the UmbracoAzureDataService - DataService = new UmbracoAzureDataService(); - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - public AzurePDFIndexer(DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base(indexPath, dataService, analyzer, async) - { - } - - - public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) - { - base.Initialize(name, config); - - this.SetOptimizationThresholdOnInit(config); - var indexSet = IndexSets.Instance.Sets[IndexSetName]; - Catalogue = indexSet.IndexPath; - } - - /// - /// The blob storage catalogue name to store the index in - /// - public string Catalogue { get; private set; } - - private Lucene.Net.Store.Directory _directory; - public override Lucene.Net.Store.Directory GetLuceneDirectory() - { - return _directory ?? (_directory = this.GetAzureDirectory()); - } - - public override Lucene.Net.Index.IndexWriter GetIndexWriter() - { - return this.GetAzureIndexWriter(); - } - - protected override void OnIndexingError(IndexingErrorEventArgs e) - { - AzureExtensions.LogExceptionFile(Name, e); - base.OnIndexingError(e); - } - } -} diff --git a/src/UmbracoExamine.PDF.Azure/Properties/AssemblyInfo.cs b/src/UmbracoExamine.PDF.Azure/Properties/AssemblyInfo.cs deleted file mode 100644 index 04347fdc21..0000000000 --- a/src/UmbracoExamine.PDF.Azure/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyCompany("umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: AssemblyTitle("UmbracoExamine.PDF.Azure")] -[assembly: AssemblyDescription("Umbraco index providers for PDF based on the Examine model using Lucene.NET")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("UmbracoExamine.PDF.Azure")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -//NOTE: WE cannot make change the major version to be the same as Umbraco because of backwards compatibility, however we -// will make the minor version the same as the umbraco version -[assembly: AssemblyVersion("0.6.0.*")] -[assembly: AssemblyFileVersion("0.6.0.*")] \ No newline at end of file diff --git a/src/UmbracoExamine.PDF.Azure/UmbracoExamine.PDF.Azure.csproj b/src/UmbracoExamine.PDF.Azure/UmbracoExamine.PDF.Azure.csproj deleted file mode 100644 index 7fe5b12b63..0000000000 --- a/src/UmbracoExamine.PDF.Azure/UmbracoExamine.PDF.Azure.csproj +++ /dev/null @@ -1,104 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {B555AAE6-0F56-442F-AC9F-EF497DB38DE7} - Library - Properties - UmbracoExamine.PDF.Azure - UmbracoExamine.PDF.Azure - v4.5 - 512 - ..\ - true - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - SecurityRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\packages\AzureDirectory.1.0.5\lib\AzureDirectory.dll - - - False - ..\packages\Examine.0.1.57.2941\lib\Examine.dll - - - False - ..\packages\Examine.Azure.0.1.51.2941\lib\Examine.Azure.dll - - - ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - - ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - - ..\packages\AnglicanGeek.WindowsAzure.ServiceRuntime.1.6\lib\net35-full\Microsoft.WindowsAzure.ServiceRuntime.dll - - - ..\packages\WindowsAzure.Storage.1.6\lib\net35-full\Microsoft.WindowsAzure.StorageClient.dll - - - - - - - - - - - - - - - - - - {73529637-28F5-419C-A6BB-D094E39DE614} - UmbracoExamine.Azure - - - {f30dddb8-3994-4673-82ae-057123c6e1a8} - UmbracoExamine.PDF - - - {07FBC26B-2927-4A22-8D96-D644C667FECC} - UmbracoExamine - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF.Azure/app.config b/src/UmbracoExamine.PDF.Azure/app.config deleted file mode 100644 index b77bae14a4..0000000000 --- a/src/UmbracoExamine.PDF.Azure/app.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF.Azure/packages.config b/src/UmbracoExamine.PDF.Azure/packages.config deleted file mode 100644 index 0d4b103fae..0000000000 --- a/src/UmbracoExamine.PDF.Azure/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF.Azure/packages.old b/src/UmbracoExamine.PDF.Azure/packages.old deleted file mode 100644 index 7ef890a780..0000000000 --- a/src/UmbracoExamine.PDF.Azure/packages.old +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 0b5a2452c2860800c00b7cc5268ac3d36c6b8ad4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 30 Mar 2015 14:38:08 +0200 Subject: [PATCH 129/249] U4-6476 Examine Issue: The CancellationTokenSource has been disposed. #U4-6476 Fixed Due in version 7.2.5 --- build/NuSpecs/UmbracoCms.Core.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 33c8a20953..656046b1d0 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -32,7 +32,7 @@ - + From 33f21e0528a9c969147e2c14a878ebb8912287b0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 31 Mar 2015 13:49:28 +1100 Subject: [PATCH 130/249] Updates build and solution to exclude UmbracoExamine.PDF, removes files for UmbracoExamine.PDF, updates links to license in nuspec files --- build/Build.bat | 1 - build/Build.proj | 45 +-- .../UmbracoCms.Core.AllBinaries.nuspec | 2 +- build/NuSpecs/UmbracoCms.Core.Symbols.nuspec | 2 +- build/NuSpecs/UmbracoCms.Core.nuspec | 2 +- build/NuSpecs/UmbracoCms.nuspec | 2 +- build/NuSpecs/UmbracoExamine.PDF.nuspec | 2 +- src/UmbracoExamine.PDF/PDFIndexer.cs | 291 ------------------ .../Properties/AssemblyInfo.cs | 27 -- .../UmbracoExamine.PDF.csproj | 96 ------ src/UmbracoExamine.PDF/app.config | 19 -- src/UmbracoExamine.PDF/packages.config | 7 - src/umbraco.sln | 1 - 13 files changed, 8 insertions(+), 489 deletions(-) delete mode 100644 src/UmbracoExamine.PDF/PDFIndexer.cs delete mode 100644 src/UmbracoExamine.PDF/Properties/AssemblyInfo.cs delete mode 100644 src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj delete mode 100644 src/UmbracoExamine.PDF/app.config delete mode 100644 src/UmbracoExamine.PDF/packages.config diff --git a/build/Build.bat b/build/Build.bat index 98b8adad2f..1b715dcc15 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -56,7 +56,6 @@ REN .\_BuildOutput\WebApp\Xslt\Web.config Web.config.transform ECHO Packing the NuGet release files ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Core.nuspec -Version %version% ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.nuspec -Version %version% -..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoExamine.PDF.nuspec IF ERRORLEVEL 1 GOTO :showerror diff --git a/build/Build.proj b/build/Build.proj index af7be44c7c..5be460d09b 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -75,7 +75,6 @@ UmbracoCms$(DECIMAL_BUILD_NUMBER).zip UmbracoCms.AllBinaries$(DECIMAL_BUILD_NUMBER).zip UmbracoCms.WebPI$(DECIMAL_BUILD_NUMBER).zip - UmbracoExamine.PDF.0.7.0.zip False ..\..\build\$(BuildFolder) $(MSBuildProjectDirectory)\$(BuildFolder) @@ -89,7 +88,6 @@ $(BuildFolderAbsolutePath)WebApp\ $(BuildFolderRelativeToProjects)WebPi\ $(BuildFolderAbsolutePath)WebPi\ - $(BuildFolderAbsolutePath)UmbracoExamine.PDF\ @@ -150,7 +148,7 @@ - + @@ -158,15 +156,7 @@ - - - - - - - - - + @@ -264,7 +254,7 @@ - + @@ -276,35 +266,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - $(BUILD_RELEASE) diff --git a/build/NuSpecs/UmbracoCms.Core.AllBinaries.nuspec b/build/NuSpecs/UmbracoCms.Core.AllBinaries.nuspec index 35839623e0..e8fae204ce 100644 --- a/build/NuSpecs/UmbracoCms.Core.AllBinaries.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.AllBinaries.nuspec @@ -6,7 +6,7 @@ Umbraco Cms Core All Binaries Morten Christensen Umbraco HQ - http://umbraco.codeplex.com/license + http://opensource.org/licenses/MIT http://umbraco.com/ http://umbraco.com/media/357769/100px_transparent.png false diff --git a/build/NuSpecs/UmbracoCms.Core.Symbols.nuspec b/build/NuSpecs/UmbracoCms.Core.Symbols.nuspec index 29f7365017..d17cf39884 100644 --- a/build/NuSpecs/UmbracoCms.Core.Symbols.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.Symbols.nuspec @@ -6,7 +6,7 @@ Umbraco Cms Core Binaries Umbraco HQ Umbraco HQ - http://umbraco.codeplex.com/license + http://opensource.org/licenses/MIT http://umbraco.com/ http://umbraco.com/media/357769/100px_transparent.png false diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 33c8a20953..a86f172c6b 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -6,7 +6,7 @@ Umbraco Cms Core Binaries Umbraco HQ Umbraco HQ - http://umbraco.codeplex.com/license + http://opensource.org/licenses/MIT http://umbraco.com/ http://umbraco.com/media/357769/100px_transparent.png false diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 1c035a4ffc..d047af2d29 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -6,7 +6,7 @@ Umbraco Cms Umbraco HQ Umbraco HQ - http://umbraco.codeplex.com/license + http://opensource.org/licenses/MIT http://umbraco.com/ http://umbraco.com/media/357769/100px_transparent.png false diff --git a/build/NuSpecs/UmbracoExamine.PDF.nuspec b/build/NuSpecs/UmbracoExamine.PDF.nuspec index ca4acdcc36..5d1afff2b5 100644 --- a/build/NuSpecs/UmbracoExamine.PDF.nuspec +++ b/build/NuSpecs/UmbracoExamine.PDF.nuspec @@ -5,7 +5,7 @@ 0.7.0 Umbraco HQ Umbraco HQ - http://umbraco.codeplex.com/license + http://opensource.org/licenses/MIT http://umbraco.com/ http://umbraco.com/media/357769/100px_transparent.png false diff --git a/src/UmbracoExamine.PDF/PDFIndexer.cs b/src/UmbracoExamine.PDF/PDFIndexer.cs deleted file mode 100644 index ef07b3929f..0000000000 --- a/src/UmbracoExamine.PDF/PDFIndexer.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Security; -using System.Text.RegularExpressions; -using System.Xml.Linq; -using Examine; -using iTextSharp.text.exceptions; -using iTextSharp.text.pdf; -using System.Text; -using Lucene.Net.Analysis; -using UmbracoExamine.DataServices; -using iTextSharp.text.pdf.parser; - - -namespace UmbracoExamine.PDF -{ - /// - /// An Umbraco Lucene.Net indexer which will index the text content of a file - /// - public class PDFIndexer : BaseUmbracoIndexer - { - #region Constructors - - /// - /// Default constructor - /// - public PDFIndexer() - { - SupportedExtensions = new[] { ".pdf" }; - UmbracoFileProperty = "umbracoFile"; - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - [SecuritySafeCritical] - public PDFIndexer(DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) - : base( - new IndexCriteria(Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty(), null), - indexPath, dataService, analyzer, async) - { - SupportedExtensions = new[] { ".pdf" }; - UmbracoFileProperty = "umbracoFile"; - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - /// - /// - [SecuritySafeCritical] - public PDFIndexer(Lucene.Net.Store.Directory luceneDirectory, IDataService dataService, Analyzer analyzer, bool async) - : base( - new IndexCriteria(Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty(), Enumerable.Empty(), null), - luceneDirectory, dataService, analyzer, async) - { - SupportedExtensions = new[] { ".pdf" }; - UmbracoFileProperty = "umbracoFile"; - } - - #endregion - - - #region Properties - /// - /// Gets or sets the supported extensions for files, currently the system will only - /// process PDF files. - /// - /// The supported extensions. - public IEnumerable SupportedExtensions { get; set; } - - /// - /// Gets or sets the umbraco property alias (defaults to umbracoFile) - /// - /// The umbraco file property. - public string UmbracoFileProperty { get; set; } - - /// - /// Gets the name of the Lucene.Net field which the content is inserted into - /// - /// The name of the text content field. - public const string TextContentFieldName = "FileTextContent"; - - protected override IEnumerable SupportedTypes - { - get - { - return new string[] { IndexTypes.Media }; - } - } - - #endregion - - /// - /// Set up all properties for the indexer based on configuration information specified. This will ensure that - /// all of the folders required by the indexer are created and exist. - /// - /// - /// - [SecuritySafeCritical] - public override void Initialize(string name, NameValueCollection config) - { - base.Initialize(name, config); - - if (!string.IsNullOrEmpty(config["extensions"])) - SupportedExtensions = config["extensions"].Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - - //checks if a custom field alias is specified - if (!string.IsNullOrEmpty(config["umbracoFileProperty"])) - UmbracoFileProperty = config["umbracoFileProperty"]; - } - - /// - /// Provides the means to extract the text to be indexed from the file specified - /// - /// - /// - protected virtual string ExtractTextFromFile(FileInfo file) - { - if (!SupportedExtensions.Select(x => x.ToUpper()).Contains(file.Extension.ToUpper())) - { - throw new NotSupportedException("The file with the extension specified is not supported"); - } - - var pdf = new PDFParser(); - - Action onError = (e) => OnIndexingError(new IndexingErrorEventArgs("Could not read PDF", -1, e)); - - var txt = pdf.GetTextFromAllPages(file.FullName, onError); - return txt; - - } - - /// - /// Collects all of the data that needs to be indexed as defined in the index set. - /// - /// Media item XML being indexed - /// Type of index (should only ever be media) - /// Fields containing the data for the index - protected override Dictionary GetDataToIndex(XElement node, string type) - { - var fields = base.GetDataToIndex(node, type); - - //find the field which contains the file - var filePath = node.Elements().FirstOrDefault(x => - { - if (x.Attribute("alias") != null) - return (string)x.Attribute("alias") == this.UmbracoFileProperty; - else - return x.Name == this.UmbracoFileProperty; - }); - //make sure the file exists - if (filePath != default(XElement) && !string.IsNullOrEmpty((string)filePath)) - { - //get the file path from the data service - var fullPath = this.DataService.MapPath((string)filePath); - var fi = new FileInfo(fullPath); - if (fi.Exists) - { - try - { - fields.Add(TextContentFieldName, ExtractTextFromFile(fi)); - } - catch (NotSupportedException) - { - //log that we couldn't index the file found - DataService.LogService.AddErrorLog((int)node.Attribute("id"), "UmbracoExamine.FileIndexer: Extension '" + fi.Extension + "' is not supported at this time"); - } - } - else - { - DataService.LogService.AddInfoLog((int)node.Attribute("id"), "UmbracoExamine.FileIndexer: No file found at path " + filePath); - } - } - - return fields; - } - - #region Internal PDFParser Class - - /// - /// Parses a PDF file and extracts the text from it. - /// - internal class PDFParser - { - - static PDFParser() - { - lock (Locker) - { - UnsupportedRange = new HashSet(); - foreach (var c in Enumerable.Range(0x0000, 0x001F)) - { - UnsupportedRange.Add((char) c); - } - UnsupportedRange.Add((char)0x1F); - - //replace line breaks with space - ReplaceWithSpace = new HashSet {'\r', '\n'}; - } - } - - private static readonly object Locker = new object(); - - /// - /// Stores the unsupported range of character - /// - /// - /// used as a reference: - /// http://www.tamasoft.co.jp/en/general-info/unicode.html - /// http://en.wikipedia.org/wiki/Summary_of_Unicode_character_assignments - /// http://en.wikipedia.org/wiki/Unicode - /// http://en.wikipedia.org/wiki/Basic_Multilingual_Plane - /// - private static HashSet UnsupportedRange; - - private static HashSet ReplaceWithSpace; - - [SecuritySafeCritical] - public string GetTextFromAllPages(string pdfPath, Action onError) - { - var output = new StringWriter(); - - try - { - using (var reader = new PdfReader(pdfPath)) - { - for (int i = 1; i <= reader.NumberOfPages; i++) - { - var result = - ExceptChars( - PdfTextExtractor.GetTextFromPage(reader, i, new SimpleTextExtractionStrategy()), - UnsupportedRange, - ReplaceWithSpace); - output.Write(result); - } - } - - } - catch (Exception ex) - { - onError(ex); - } - - return output.ToString(); - } - - - } - - /// - /// remove all toExclude chars from string - /// - /// - /// - /// - /// - private static string ExceptChars(string str, HashSet toExclude, HashSet replaceWithSpace) - { - var sb = new StringBuilder(str.Length); - for (var i = 0; i < str.Length; i++) - { - var c = str[i]; - if (toExclude.Contains(c) == false) - { - if (replaceWithSpace.Contains(c)) - { - sb.Append(" "); - } - else - { - sb.Append(c); - } - } - - } - return sb.ToString(); - } - - #endregion - } -} diff --git a/src/UmbracoExamine.PDF/Properties/AssemblyInfo.cs b/src/UmbracoExamine.PDF/Properties/AssemblyInfo.cs deleted file mode 100644 index ddb6fab57b..0000000000 --- a/src/UmbracoExamine.PDF/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Security; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyCompany("umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: AssemblyTitle("UmbracoExamine.PDF")] -[assembly: AssemblyDescription("Umbraco index providers for PDF based on the Examine model using Lucene.NET")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("UmbracoExamine.PDF")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8933a78d-8414-4c72-a74d-76aa7fb0e9ad")] - -[assembly: AssemblyVersion("0.7.0.*")] -[assembly: AssemblyFileVersion("0.7.0.*")] \ No newline at end of file diff --git a/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj b/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj deleted file mode 100644 index adc9b175fc..0000000000 --- a/src/UmbracoExamine.PDF/UmbracoExamine.PDF.csproj +++ /dev/null @@ -1,96 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {F30DDDB8-3994-4673-82AE-057123C6E1A8} - Library - Properties - UmbracoExamine.PDF - UmbracoExamine.PDF - v4.5 - 512 - - - - - - - - - - ..\ - true - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - SecurityRules.ruleset - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\UmbracoExamine.PDF.XML - false - - - - False - ..\packages\Examine.0.1.61.2941\lib\Examine.dll - - - False - ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - - False - ..\packages\iTextSharp.5.5.3\lib\itextsharp.dll - - - False - ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - - - - - - - - - - - - - - - {07fbc26b-2927-4a22-8d96-d644c667fecc} - UmbracoExamine - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF/app.config b/src/UmbracoExamine.PDF/app.config deleted file mode 100644 index b77bae14a4..0000000000 --- a/src/UmbracoExamine.PDF/app.config +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/UmbracoExamine.PDF/packages.config b/src/UmbracoExamine.PDF/packages.config deleted file mode 100644 index 72c8a05f8b..0000000000 --- a/src/UmbracoExamine.PDF/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/umbraco.sln b/src/umbraco.sln index 78202fd38a..30a2e2dd19 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -33,7 +33,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec = ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec - ..\build\NuSpecs\UmbracoExamine.PDF.nuspec = ..\build\NuSpecs\UmbracoExamine.PDF.nuspec EndProjectSection EndProject Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}" From 73d7cd4856636265ae88e511e81fc8f4c0620aa7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 31 Mar 2015 14:27:18 +1100 Subject: [PATCH 131/249] updates sln file to remove the PDF proj and removes the sln folder containing umbraco examine libs --- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 ---- src/umbraco.sln | 10 ---------- 2 files changed, 14 deletions(-) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1effed5ac0..b29bad34d9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -625,10 +625,6 @@ {651E1350-91B6-44B7-BD60-7207006D7003} Umbraco.Web - - {f30dddb8-3994-4673-82ae-057123c6e1a8} - UmbracoExamine.PDF - {07fbc26b-2927-4a22-8d96-d644c667fecc} UmbracoExamine diff --git a/src/umbraco.sln b/src/umbraco.sln index 30a2e2dd19..6fb7afde81 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -25,8 +25,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{B5BD12C1-A454-435E-8A46-FF4A364C0382}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UmbracoExamine Libraries", "UmbracoExamine Libraries", "{DD32977B-EF54-475B-9A1B-B97A502C6E58}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec = ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec @@ -84,8 +82,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests", "Umbraco.Te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UmbracoExamine", "UmbracoExamine\UmbracoExamine.csproj", "{07FBC26B-2927-4A22-8D96-D644C667FECC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UmbracoExamine.PDF", "UmbracoExamine.PDF\UmbracoExamine.PDF.csproj", "{F30DDDB8-3994-4673-82AE-057123C6E1A8}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{D24664A0-49B7-44A8-B2A5-5E5B1CCB7A58}" ProjectSection(SolutionItems) = preProject .nuget\NuGet.Config = .nuget\NuGet.Config @@ -169,10 +165,6 @@ Global {07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.Build.0 = Debug|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.ActiveCfg = Release|Any CPU {07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.Build.0 = Release|Any CPU - {F30DDDB8-3994-4673-82AE-057123C6E1A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F30DDDB8-3994-4673-82AE-057123C6E1A8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F30DDDB8-3994-4673-82AE-057123C6E1A8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F30DDDB8-3994-4673-82AE-057123C6E1A8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -180,8 +172,6 @@ Global GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {07FBC26B-2927-4A22-8D96-D644C667FECC} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} - {F30DDDB8-3994-4673-82AE-057123C6E1A8} = {DD32977B-EF54-475B-9A1B-B97A502C6E58} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} EndGlobalSection EndGlobal From cf1c38dcaafc09a9556d97f6215105a7a39368cd Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 31 Mar 2015 17:04:32 +1100 Subject: [PATCH 132/249] fixes build --- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../UmbracoExamine/IndexInitializer.cs | 45 +++---- .../UmbracoExamine/PdfIndexerTests.cs | 116 ------------------ 3 files changed, 17 insertions(+), 145 deletions(-) delete mode 100644 src/Umbraco.Tests/UmbracoExamine/PdfIndexerTests.cs diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index b29bad34d9..983a9a4780 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -509,7 +509,6 @@ - diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 6c81bdb3fc..7196d59c0c 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -1,12 +1,11 @@ using System; using System.Linq; -using System.Net.Mime; -using System.Xml.XPath; using Examine; using Examine.LuceneEngine.Config; using Examine.LuceneEngine.Providers; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Standard; +using Lucene.Net.Store; using Moq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -15,9 +14,9 @@ using Umbraco.Core.Services; using UmbracoExamine; using UmbracoExamine.Config; using UmbracoExamine.DataServices; -using UmbracoExamine.PDF; -using IContentService = UmbracoExamine.DataServices.IContentService; -using IMediaService = UmbracoExamine.DataServices.IMediaService; +using IContentService = Umbraco.Core.Services.IContentService; +using IMediaService = Umbraco.Core.Services.IMediaService; +using Version = Lucene.Net.Util.Version; namespace Umbraco.Tests.UmbracoExamine { @@ -27,11 +26,11 @@ namespace Umbraco.Tests.UmbracoExamine internal static class IndexInitializer { public static UmbracoContentIndexer GetUmbracoIndexer( - Lucene.Net.Store.Directory luceneDir, + Directory luceneDir, Analyzer analyzer = null, IDataService dataService = null, - Umbraco.Core.Services.IContentService contentService = null, - Umbraco.Core.Services.IMediaService mediaService = null, + IContentService contentService = null, + IMediaService mediaService = null, IDataTypeService dataTypeService = null, IMemberService memberService = null, IUserService userService = null) @@ -42,7 +41,7 @@ namespace Umbraco.Tests.UmbracoExamine } if (contentService == null) { - contentService = Mock.Of(); + contentService = Mock.Of(); } if (userService == null) { @@ -73,7 +72,7 @@ namespace Umbraco.Tests.UmbracoExamine .ToArray(); - mediaService = Mock.Of( + mediaService = Mock.Of( x => x.GetPagedDescendants( It.IsAny(), It.IsAny(), It.IsAny(), out totalRecs, It.IsAny(), It.IsAny(), It.IsAny()) == @@ -91,7 +90,7 @@ namespace Umbraco.Tests.UmbracoExamine if (analyzer == null) { - analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29); + analyzer = new StandardAnalyzer(Version.LUCENE_29); } var indexSet = new IndexSet(); @@ -113,33 +112,23 @@ namespace Umbraco.Tests.UmbracoExamine return i; } - public static UmbracoExamineSearcher GetUmbracoSearcher(Lucene.Net.Store.Directory luceneDir, Analyzer analyzer = null) + public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) { if (analyzer == null) { - analyzer = new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29); + analyzer = new StandardAnalyzer(Version.LUCENE_29); } return new UmbracoExamineSearcher(luceneDir, analyzer); } - public static LuceneSearcher GetLuceneSearcher(Lucene.Net.Store.Directory luceneDir) + public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) { - return new LuceneSearcher(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29)); + return new LuceneSearcher(luceneDir, new StandardAnalyzer(Version.LUCENE_29)); } - public static PDFIndexer GetPdfIndexer(Lucene.Net.Store.Directory luceneDir) + + public static MultiIndexSearcher GetMultiSearcher(Directory pdfDir, Directory simpleDir, Directory conventionDir, Directory cwsDir) { - var i = new PDFIndexer(luceneDir, - new TestDataService(), - new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), - false); - - i.IndexingError += IndexingError; - - return i; - } - public static MultiIndexSearcher GetMultiSearcher(Lucene.Net.Store.Directory pdfDir, Lucene.Net.Store.Directory simpleDir, Lucene.Net.Store.Directory conventionDir, Lucene.Net.Store.Directory cwsDir) - { - var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29)); + var i = new MultiIndexSearcher(new[] { pdfDir, simpleDir, conventionDir, cwsDir }, new StandardAnalyzer(Version.LUCENE_29)); return i; } diff --git a/src/Umbraco.Tests/UmbracoExamine/PdfIndexerTests.cs b/src/Umbraco.Tests/UmbracoExamine/PdfIndexerTests.cs deleted file mode 100644 index 2df3eb9059..0000000000 --- a/src/Umbraco.Tests/UmbracoExamine/PdfIndexerTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Xml.Linq; -using Examine; -using Lucene.Net.Search; -using Lucene.Net.Store; -using NUnit.Framework; -using UmbracoExamine; -using UmbracoExamine.PDF; - -namespace Umbraco.Tests.UmbracoExamine -{ - [TestFixture] - public class PdfIndexerTests // itextsharp is not med trust safe so can't use hte base class: ExamineBaseTest - { - - private readonly TestMediaService _mediaService = new TestMediaService(); - private static PDFIndexer _indexer; - private static UmbracoExamineSearcher _searcher; - private Lucene.Net.Store.Directory _luceneDir; - - [SetUp] - public void TestSetup() - { - UmbracoExamineSearcher.DisableInitializationCheck = true; - BaseUmbracoIndexer.DisableInitializationCheck = true; - //we'll copy over the pdf files first - var svc = new TestDataService(); - var path = svc.MapPath("/App_Data/Converting_file_to_PDF.pdf"); - var f = new FileInfo(path); - var dir = f.Directory; - //ensure the folder is there - System.IO.Directory.CreateDirectory(dir.FullName); - var pdfs = new[] { TestFiles.Converting_file_to_PDF, TestFiles.PDFStandards, TestFiles.SurviorFlipCup, TestFiles.windows_vista }; - var names = new[] { "Converting_file_to_PDF.pdf", "PDFStandards.pdf", "SurviorFlipCup.pdf", "windows_vista.pdf" }; - for (int index = 0; index < pdfs.Length; index++) - { - var p = pdfs[index]; - using (var writer = File.Create(Path.Combine(dir.FullName, names[index]))) - { - writer.Write(p, 0, p.Length); - } - } - - _luceneDir = new RAMDirectory(); - _indexer = IndexInitializer.GetPdfIndexer(_luceneDir); - _indexer.RebuildIndex(); - _searcher = IndexInitializer.GetUmbracoSearcher(_luceneDir); - } - - [TearDown] - public void TestTearDown() - { - UmbracoExamineSearcher.DisableInitializationCheck = null; - BaseUmbracoIndexer.DisableInitializationCheck = null; - _luceneDir.Dispose(); - } - - [Test] - public void PDFIndexer_Ensure_ParentID_Honored() - { - //change parent id to 1116 - var existingCriteria = ((IndexCriteria)_indexer.IndexerData); - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - 1116); - - //get the 2112 pdf node: 2112 - var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); - - //create a copy of 2112 undneath 1111 which is 'not indexable' - var newpdf = XElement.Parse(node.ToString()); - newpdf.SetAttributeValue("id", "999999"); - newpdf.SetAttributeValue("path", "-1,1111,999999"); - newpdf.SetAttributeValue("parentID", "1111"); - - //now reindex - _indexer.ReIndexNode(newpdf, IndexTypes.Media); - - //make sure it doesn't exist - - var results = _searcher.Search(_searcher.CreateSearchCriteria().Id(999999).Compile()); - Assert.AreEqual(0, results.Count()); - } - - [Test] - public void PDFIndexer_Reindex() - { - - //search the pdf content to ensure it's there - var contents = _searcher.Search(_searcher.CreateSearchCriteria().Id(1113).Compile()).Single() - .Fields[PDFIndexer.TextContentFieldName]; - Assert.IsTrue(contents.Contains("Fonts are automatically embedded in Word 2008")); - - contents = _searcher.Search(_searcher.CreateSearchCriteria().Id(1114).Compile()).Single() - .Fields[PDFIndexer.TextContentFieldName]; - Assert.IsTrue(contents.Contains("Drink the beer and then flip the cup")); - - //NOTE: This is one of those PDFs that cannot be read and not sure how to force it too. - // Will leave this here as one day we might figure it out. - //contents = _searcher.Search(_searcher.CreateSearchCriteria().Id(1115).Compile()).Single() - // .Fields[PDFIndexer.TextContentFieldName]; - //Assert.IsTrue(contents.Contains("Activation associates the use of the software")); - - contents = _searcher.Search(_searcher.CreateSearchCriteria().Id(1116).Compile()).Single() - .Fields[PDFIndexer.TextContentFieldName]; - Assert.IsTrue(contents.Contains("This lack of standardization could be chaotic")); - - - } - } -} \ No newline at end of file From 3d09157091cce3b79ebe5c32052aaee1f20b0575 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 31 Mar 2015 17:06:13 +1100 Subject: [PATCH 133/249] fixes build --- src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 3cb4f6180c..ff23c57976 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Models; From c35090421982f5839de67fdb6f57fd054ceae158 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 31 Mar 2015 17:07:35 +1100 Subject: [PATCH 134/249] removes umb examine pdf test files --- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 -- .../UmbracoExamine/TestFiles.Designer.cs | 42 +----------------- .../UmbracoExamine/TestFiles.resx | 12 ----- .../TestFiles/Converting_file_to_PDF.pdf | Bin 118488 -> 0 bytes .../UmbracoExamine/TestFiles/PDFStandards.PDF | Bin 54932 -> 0 bytes .../TestFiles/SurviorFlipCup.pdf | Bin 84177 -> 0 bytes .../TestFiles/windows-vista.pdf | Bin 99838 -> 0 bytes 7 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/SurviorFlipCup.pdf delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/windows-vista.pdf diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 533fc5c491..43a81fb76a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -579,14 +579,10 @@ - - - Designer - Always diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs b/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs index 79e2fa8c53..ce86c80041 100644 --- a/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -60,16 +60,6 @@ namespace Umbraco.Tests.UmbracoExamine { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] Converting_file_to_PDF { - get { - object obj = ResourceManager.GetObject("Converting_file_to_PDF", resourceCulture); - return ((byte[])(obj)); - } - } - /// /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> ///<media> @@ -83,26 +73,6 @@ namespace Umbraco.Tests.UmbracoExamine { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] PDFStandards { - get { - object obj = ResourceManager.GetObject("PDFStandards", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] SurviorFlipCup { - get { - object obj = ResourceManager.GetObject("SurviorFlipCup", resourceCulture); - return ((byte[])(obj)); - } - } - /// /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> ///<!DOCTYPE root[ @@ -148,15 +118,5 @@ namespace Umbraco.Tests.UmbracoExamine { return ResourceManager.GetString("umbraco_sort", resourceCulture); } } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] windows_vista { - get { - object obj = ResourceManager.GetObject("windows_vista", resourceCulture); - return ((byte[])(obj)); - } - } } } diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx b/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx index 5e4f836470..7dfde4fbad 100644 --- a/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx +++ b/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx @@ -118,25 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - testfiles\converting_file_to_pdf.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - testfiles\media.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - testfiles\pdfstandards.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - testfiles\umbraco.config;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 testfiles\umbraco-sort.config;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - testfiles\windows-vista.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - testfiles\surviorflipcup.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf b/src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf deleted file mode 100644 index 94dce8253e70a9e649b64caf222b9de4ee284574..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118488 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 diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF b/src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF deleted file mode 100644 index 060beac5ef10b2bfb92749b98a2f36b81ebc0532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54932 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

#mxhR3M$+}1|iILK7WdiP9i#*JIty|9-2 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 z zlXE^^FTzb}WPd%-Y zX-7SJ1OsHJA>$}(%P8^IAe2xZaFj%(hkcNQskmMQ|J6F6onUS=20UBg}e2PO&`A5eZ zwAFT8{qpb%HFhfA?%Kudkx6H@{@A81PAu9KL8jm_`z`1px#@F5Gzr=QOtfss)rNJl z;mK$Z%iKoMnxbNYbBQI5HJ7m?Oa}?c#$PyGf}19)vA*4Kmy(KS65GRek;i=i~>v?EqP(|KY4i7Vfjz|m`;3VGYbxa|Ea@(;z|6jKv8Jq)A zm;QnkdFG#mGji2{8*XWqN#-NHAz#h8=UN+ZTEFy-k|c&ac%sPa$nJqrsgFv7Ly~+K zTFY?P-B?VH0e)HDFUJqzYyirE@sx*y? zZgNPhmpEnHgeRS|DMBh_u1X`vy{wJc1c!Iec1#XL=D*(+Ja`Ye^bnhHdl5FOQEE_l zx>YKJ@BTtg9nC}VA!#{6ib%W=;gjh}GQoK563@j#I7Zsxv%`y%f}fOSh|uo)AB4Pqeh(c^7eTRuZ*HrQcVsun_ z_KJF*ucoArW-Du#AUv4RhcTNcq4h*jG!Yrvym73G4~NltVOxfKyYkel^}cwAFP zq1YAJ>(M8JqvVUpPDJ_(<7;&ksiWMYE}5e+odn~D9G;-%DKAlC#~0x9WKQSXItFRWC(zED znAzKAXyaP;bv_ybM0_+^Q7{x&_O)H+IXU7yYL(}q_`W-1snps~^rO#1-g?ZV8vhb& zBE9%i#wTqpWvu6%^NlPAO&GXB76zMR_kgUVYS!qi79&I$(41xJJD$(7`Y6PHj{sK? zqlO!Zjog(P0G3$M?#3Z_iR3HSN@VUyg>`3SF-n`lLQp>vVAO{&4n4@D$Nha1&iozd z+xoy+gYV4^d^&nXn+Q1=bB>7y@7Awm`UW9g8vAXjuhs;)lWOHhUa9EuG}9XEI34}~ zEnM+hZNFNj1mWd10?K(PXv-R*k}^*{-X=z5M`$=!=0dBq`U)RqnJP{M{}dpqEV>C@ zQIbA@wAHF-AniA&pqOkR1ZV**$1j(O3htkF6C@Ac0`Xs%;!~g0DtwKV3(rPh1RTOY zBg2UDpS=D4rl8}ohTe!iEWALI&T-#*Msb{8DeyPM7Ep{rW~@Wk3;xCx1~O1ZWI-TG z5^_-O*xbp~SQXQkH@w8orPOfHNQ)7Kd*M%MB~bPd({r^a3okG@3-uD)1QvL3I zW6Zu6Vys;OZ}GZEvx~+w>%swIu{m3WNY-+I&@!GbDTVqLb-_TUqSu6?1SIH(_wkgz z@=(d$(m?>Hnr2%znE-6Sm(zvkY?VK`6K1{OvLux{qn|PgFY6#wk9?VKe-WmuUXw4E zzt*GJ#%r_+n`&gJW6X`5gJ!wwn>aM&B+A1~Z0=f?^Cy-M z&{GCjyZ~vWahn_G?H5@_hm)EKZEEP2)Ysz?k1b>Lf18K@1#Mj7a=5Ir55Qv}qM{d!j4)!RQTroK<-zSJ00fYikJpPyN zSlLBwwc^I9wA0IsO*rz4G55T7sza~0^4nmQRq=M?j?U-pq$_H!*SX8?*9W0*$LIZY ze<*HO_shNj%b>>P9_s5o>(;feFAyzc%|*t{P3^ZE{vNEs$?;Tpv)R+_d3!|d(<`iK zKF2nD!_S_*vs9_3{$8sRpK%<^m(bHp8DT*50uv?*s%)NxV5eJ~wRh;*4tLwi5gn`Vpc@6mq>k2#yakh~E4 zn|D9E{}?90exIY;^J-L;9BO?Q>0(d!`!DmXLniAX8nP$VZ%y2gVb zo*6=pq|V6-WCa$QvZM|sm1NGP_bHX4SqZL2aa9&75fYjcy_H4EbTVY*FA9n`$5|$8 zE+z?=F5C&w`8xHio zzX)osY7MZ$%KqQE-YZeNV}tnSGqA8Cmfriy%|3WhJ1vA^l;+JA+S(nELQ0S+{}wNc zqI2Mf4gA7aqQ5ZPKtMJA0hZ|8S5$B*FhCM>TT`+jD%wnJqK=g*h(4)O? z4ZPu;vC#cSq3$#S0cAI$UWjQgJL?dF( z1-nM!0`w6N;_?UH*=F*?!6`Qz4Al$nj#9+dSAibE@J`ACN_FP)k!Qy*Ft@Tqh|ytF zOE1$r92sfbJ9P$uuv!VDZhvYR3_Il?RrJ0E#-DzoV;ePeDFN8TS?Hf{ggE`bMliyQWJ)u4eQE7w~6<^xKW# zP!>3^75tgn7ZJO?n<=kS?0~2XiUJC4^i>d74P(9v~(Y$&u$N?vitY&ea>ls=rTwH>3b^N45P;bH37E{me}WSS(}7i#D${I0_{`>kRbs z!5Aau0m+N-iqC!>wCQ4O6B^Vqowv{lImGl6yIMm&Zvsts<@Si}HtNP+u_Nvw+y0K< zDQ6IdSprm|t5FHpYgSM%QakTLvSb17T~(^eh%ji?k&@s*k?7K{BwR_W4T#ecl-F2< z&we240edkh5kn%>5VZ0#BW6H#=LFzN7@s4-Y;qoqCl1v}wv4qDaSs8UBvT&u2aIVq zTJ``s@!uIX9l1uMhDOR`bTv(sP<$bY6A%k7{@zDEKsa)eDI)+0q$qrmdizbk*$1eV z%?;QUl`Rz-T#w@&Ep!t{7|TP@^MirY(r;b&OD5`wb5wTr$SP!Trh824KD^;O5jIq8pUE)Q zGf66-=LIL+5cNcUaR`$S5sZ%qsjSpxInvyNfq2zq!IZ|0T=kXMuxAGS5GPY>rW<%P zP~x6^UnFPLytLpJ>9CQ8D1P9>??ZB8RtGYF(<=k*j zobqJ(WdpA|@Bg=urTBDxxgS66$vX zU1Hv})7?tP#H~?t$I@1eR@_#h>|jY5AdHU2!hy9%Lfz)THWJRjFp8eR*nVhsS0TEb z#?YxD>^CtlE|qo0A*^;v+b3JF5O?4`LU!%(}%zdu+* z8zzPnbnl>6k(QW(_|q7$Tq~}EmGG^rql8b{OrG5?s<6-O=-(=){V=qG=-K+!ms;PL zN-yQswt~2@*`_QNE{)d9Pv;$}OZMUaErmXXKf0p2$4|7W3O%E92~@s6@|UuR_cyCx z4$i1co)$={W_#Y{!U6OhC)mOr8Mqw@zt{`I7{t9`#qEVBy4oderP_#i4{+K*Vi9P} zrovtT?KT7d)`R9pZ_!gM7I7|NYhgno$e&N6Ogs}#wg*%9;m=w6b3~ho3l)A| zjM-Qb3OohH<=J!B;sa877$fMi)Vsx&RC>x%B*Qa!SWs%M7PcZPfGKK(lPX~4nCIAV zr$&USkZQ_6Gm~ocOsF_|h6MdvO(WTK@xPY&ljMhaAt2;y(PIe+k~8V>X|&^s)!zN0 z{#l;-Peac6nL@U*01X9y)Lh9Ff(7`!-(;Z(n8`Y+ED|@_^Xp3``vB&KKnMfw0C$oI zavgPBa4L;Vw{BNI)4yA3%;iDr7ryT!da%gE0d&$BKA_y6%9=K! zyV)>utAO17MM)}VP5HY<;ZgyFZ!h#p#_?M2W16G`f`B>m8kt3w2bnc5R1cLv-2}>P z%2I|lq8$Hx;16xwZH2;ftI_L%6M5X~jDM)Ovm~`NlH|8*!Nn51qbPLZbcasqZlVg0pU;APMRiiuM;;h)XYf;bY+#L)^ zU(O)Jn0pQmI-knVa*&Z8!tv`VP6(;I#R0|>NvuEV-a{;{R&Y9qhp0JWvELXoK*EnP z%$}YXk=-yZFn79yv>*~diXLV9VpA~I-i8sw>j?|RXRvVXX_i;fE`<_D%e)-1#>MV+ z7ttc$rEJQ0uv)g00L6i-XG7~f(A9+xYe;FsM)g%a>=z#Ls3R}Tk5X*uk~=Gohp?HAXqyi7_9fz6MaW;5)$;dEbghmT(ZetF;izy z?f39=fhmC~Q%0qj=|+j2HAWa)>greHg!?eG!3}Q2dywY*NT0$kZy)^1y&!4N4~%s^ zeSuh{5a3v@>QQEo-!fY#m+izh6doFi#CHi+>jbZs6{)D7-DiZXU1ldy1nn&7zVlhj zeAofLM<{r-d$B8nlRJ=fF*E+T|0XvsZEBAU4*xM&Yo4#)`ksS@(2eKwfd}~$9l&3Q z#z*E3D)UGt(4ilsBPb}WK+Jm2E?b;)+sR+$M31{6iJ(zK0(W<;2CQ{5}-W z|HELhwc@0?Ivx^RjzBw+d1zWnuKKnoJP4FH+ImTbfyf2QOU>w zAOyKFX5xq%$OLvrPms~ra1?{s4N;pjem4hcU3UOib?^BcAi@&$uCQY1OfMs#*vwSC zwhs@MR0dZx{D{6O(Ab7g-mWCY3g%^jR*>)U!1yOtLW1CQQzy8(JLgmSOg%$(<<1M- zyBV^)Q&C?|$Qzw308)xAOiy3}fwzOeaNTzrNQ|9XiORGpQNo?|kfc@3K>UqbEh(R^ ze*ma8#m}M26V@Fr0v{A>UR(RPQSAfb;2^kii@}$0+kjAIF`~gf8X+{9UHb&xirpG2 zPMtHNdKu2mp>IQ=yZ3t<&$8TzN77RUwUNGH&dd5T`3lxp3%`pA3fXZi(Zy*1$yy~l zgP{xk#4#8j!(`oaf}qGVCq-1VnvD-gU@jbvWDYFRZo6zX5R58wTmlzNH+Yi;quI95 z%Iv#Rkv0tJq&b~?nTJ#0M*VftRT|Rzq2!-B&$|jXGJDKM8Ri8MMIGQN3;60$@LaUZ zIn_(Jbf960#tqpCGckQZKfCR(2FgU@+T+HJM;%sY0;qXCbQ57-qw)C#%av6I6h%pi zU2(KOaalA=JLMBI>Txp*Z9lg$KOTAjrF20!(0*W$I(J(5fewaZ%6zH3wJr}X^R_@o z3pps_A1szLwN~(RAX2E;Lp5N2sDx;%%cU#;Ts{bPznaH>ZDJ|RXrs3cjNOLWxY2<^ zkRP}~J%_<$W^}W#2XJiy&ST05=}{5AFe28T4?WCyyICrMkdm?n#D0rRxiL%mL#e(f)@@syw_0!vCYl) ziG@BcyF*dXJ=Pma9NpD-gNHs6us>>Q>TKW~I;BOM3NEejq2Xzl=Xitc4&o=F5}Mfq z8Dvk|PBbI%9U1k&v>Ku!PJuPi5ujwe=&mN+2hLer@J>hrl+NvV0JpH+g2)1t6>Q_WDtyOJ);KBi z?fx$3N5u_;$rHVY@tVWd4yD_U$<&RW<$pa=Ql+9X-BD0 z!#Pg=+ydr95(B~%#=Xme03Qyi4@GASjj~;6>Jj2J%o6)okFIu-L4^sv7=J4%D2yT%OR)>i`ReU6hiGi#$X0R@PS^rG_RSP| z5JqS|wNGT?iK5zAFO6ZB^SF?s6u6;$mciqjB{Gf^Hl%h3yT08OUAQTpq~=qQ%g{+K+NOfcK+dLv}pp6}XQucvW<^Y75q zExFou?Eu0qpk=BUwWZ(Q~|X2b!Lq&(G)i{_b!4Yn@jo z(u(GP9Pj@g_?Th(*9529_3(PWx_uS?3Tk(Jn2hMXd|qy%yZwH*|NQu;rpi$YV>@eD z3|jiR`?mUvaUN6B_j|mKS3$<-R!sf;cxCr=jrQBE`c%+xa(Cmhi}5j7HDq*AdfUu$ zb2qZD={iJ@?(pWHoBgzcQ#(kn+b)2E`?u)4IVr4bDJju%KI}G_Z__|N*Ar3awk0_% z%pf%fy_?7Tb|aWJXN-X_)<}{DL?*|xkO1b{1DmtL`W6=M>=?htDJ-rwt`})k%g@~| zcPsA938Qm03{Mx&LS~jcs%cENqHHH>-~RN&oYNwCrM?|+7)8PgL>gI@+r{o20d!`Y zuY>mmCuS##KdA`0^762JL8`D1FC)PES!nf@wBbl=m9<(H&sHI=^EQRsn+w(=`6*$& zxp@Aai<#@MmgY&7fw<*Hl>zaO_=6gtiK+5_v9?+QX=v31}tuW+Ou3N3!I-jLPp2`3eqOk5@uamj{W5kPe9H+ zNy7*2>rAT9Od%c=$_(OAOLSiW@^G@-wa%W14(i}DCBW~42~7Ym7$5y(->=UOrYp8b zLpV14WF#2Vt$BuH3W;4|!5vA#D@4c7lUtYeXDz~5gBZhwrXP-@+c`G&2mz-g_x`zT zfa>4De;@5}@wF((aJb(?*>I(1b&!0pyd8R7qb@}#?#x#`%wa`5=x3~I;CEiIsy-G! zSC;*PKf#UQ`ltzugrYSApY1jv`aRPyH2YP9J@|pv2Cj5knkOU7E#2^bv%jppL`Jj9K zVDvd>zWJixxY7Jb%MxF&)amB@`-;$P6^_#ouJA&=F1610M7` zCR>wo?(Hab{N?#+0*S+agZZ=LdeVv%7-6&9)Iysq4DFnSX{~3#M)uJ6v3eb}4#Rq8 z81_k#3g^rECjW$s|1eU>zD)#wv?40{q(=C9DiY=7D_{DHhN~Q8E^x(m2VcEHFP!?W zmR2ro5Xhni=skc|&{>&eKR$N|iC*0?RuoIvz1n0z$bIV&@~$fQSy>;DbGDb!Rr~pD zZrf%Vg6npW(ut-c3I5!`R|^3Yn}qI94CA#dP>Gmc@Ri$H8^p*X<%K$TLtE&GKUYyV z{#7>GGC;;Yr@5&7O`&@k4hk6Z+9K+zr`eu98ZEIGlEc9J033|Ax*?pkw*jgLJp5(r zfQRfjR2;df8(DWmAk+SgkE+65Ri<1$VvBAD4JK$EmDUMsY<5@OCXsZ@-t=FWqu&Xn zfuN<wkhX{S7kmEIbdv{uszcs^?tXBzC^9qqHSPn-;u3yr6P%zC3 zB^rPRqxbrr8Ob%@&r``GY&<6W=E)w9B}C8WQBkR(ab7kG8O6nP{YZVy0zLPomyEGy z4}7$HZkaGo8wak2f=0->7@~77!dYHJq$K__EJS`$V~aeFP2(%s(uT}}S8RDGpY ztCAuDug11w9~%PPyap$m+!BUv;hTA9XNTOB^f9neSKgX*q1%-_9OD2PSl*@n8i`wC zV29?aA|^4Yga!PY{;eP&jCd9uleorkHGdF9C3!10n!;0VHp3v)!|+PWn-NY8Mo7G? zo#i8DZvqjaiLdJoI`-kNnq@z>^iP3*0$nXyed!U)PvIM^CD?^?A3YNdF0)#G{yV6* zB*R83=C4GARI}_&o14Yh_lvu40ZexHY#tcgAS{QXYFx@*0VnSePHG)&U=@MU^*?|u z@>PahZdof<^7_{Y_Zr7-@b(`EcG2m-st#M5MQzLOrQ9dJ3*EiTKj1kn)I*6=#^5zd z!hR?s;6C{fej^N=%WckSL^(DLUYL3DcNneEDaWxOpQ! zD@YjUX*!&>lo#bnf6NM4`?s1Est1XS1b2hoq(lTU+rdSk$$Hhw3`H6eP*lEDfkNDa zs5#z8jtTp^OlbwQ&G-z|Dth!Iep`UZ0$Spa@m1VJgEf|HaR0iPp~yF37anhZCJQhi z6pb^vY6A4Q7XKsMpm&d}B;NFyox7jAWG&*v5xiiK1h+4T6~fa$jThj#DIt$;CsUQN zQpPBrM{xLH9TgJLntX(=31TWls%DG6U>-N<5&L?)KnW@gxCE%ESZZ}=Jhvf)Y$_kfwldYs1c0iV^UV@M$> zjJwH`FfswyXMPc-A^_lKV23vbn~-es zRTp&q;my2Y7X>Z83=j_YBpsZ*zi*b>t!$7VW4Z$Rf(1$E-n&S1_Vont!N4;6)_B5H zLRen*4fVTK;zZZL731>lOkEO_j7ebt+LP&jx}d*E?S?KZ0to)Ls6+_U`ZG9?BI3kMfPRxN z+u#ZbSCF`4j;`uru=uwcd!D{OC?GP4mf%eM-dY?SVJFl~FlKOl;DdH2H}(U|fNntdT~ zDvK??$|yhmdqwCe0Mf*L*d?x5{~J>oscUq7(waue||GYsPM5QEKS4lvcdKarsFH z+M=}XO?$q*H;Tq$@aFUA=#!&9E2}Ms6)9|Bexs%ytgW$%HHg5dW}!W+rWY%+zwn{Z zzPoNxotBi64he`6a!d_V{~z9|{n{-WEQg8_p|zpG6n>a#=t|0{l9_^Z=G=tmje1i+ zqbC1k1L@3Q#2{KTzXX0#H6kUd%$$q97M$C2^-#{VKZg`)&@^u|CL}%^+~=AE3|OYe zPF-tsdKm7qz@5l($D|B`cmLk3?z8EAVV)3a-wfju@F98hdPTC8GUOGN8-4HvkVY=! zzL+`b<)&Dirx$ucX|;Po+;eb33J>QSs+a;-r&uFviB8qT z#$I=H*dbj)wNB0?ol}c??W*kZMXv*i!ie}I>DxPrrCUzF0o+MW%2LQUS@2a8!PL&I zoW`T`dG+;hZJNgBzspW=#nl0pn!Y^M=Galc>#=5{Zau}8C+Bp#o=vXnwu$u=-gX}l z$!Tk(nz4zjaaH6la)QGate)|~flzBWgv#oR%86Ei8*GCHT6-f^Zuw4o0M#s14L`dV z7U&-FA*oz|xc)#{kdiDL>#Eay&k2W{>ao?w!EWgVUn7G;-Gm5w93`)vAK8wK0`>0@ z1<{sdbRNR0&T4oyQ0ojJ*l++N84`holB_8SE!28#u@+Jv)n5bJ`0+=x2!5ggQI+Lw zY%z^hhTuw=o5PJSD~}AM2)fJZ1t{=WEcXXzw8~yoWZUg-aqBtS8vvvE9&u6UquIez z@_#v!?f2me!mj>QKjMa}QoMx>Baa-}f3prMy~o7#okSFtI1j?Zzm$M$DT4c^&0-Ro zpl-F$8~i2fnlMVH5+EFM8*0n7O_i;P*0_hRN?e%);E;Bo>WdSBgkn^>d#=g&$r^Y$ zG#WHozE1kQJx^uYD-9XNvdML;KPpy02Nio+?vTtNI&H<54toAZ{5}YU9aEnmkCvmH zpnu3vIvyymoIrGwSj6MCl9rTI+~N6wMJgsXaa*)gccd?z7Zo3Lut%TAnmDVZqM*r&Kp`%xmay! z+?6tDqH35Ns9fsvVH3Vu@wUz@(Oas7!zt9=H_$K-Ue{EWohO(?(qWXS<#ld zmky;{SH4a!W_Z|ofgZNKJ8mr(5qxDU94V%LrW^z#`ZLlFvl(&P*DnSx*DP2C1v0V0 zYmNs@x4@Xep!L*)ltV|i)As=^rB zweYh}P^u&EfhkQGc05+gZaNn$k2P)?U{Cd;x*lJl_c%8lR)VEl9Sd~008=FBV+0i2 z2!LIf+Q;ftET@9k zvg=WpJNbvRZ90V)l+xl-oX}?)1EZSR2-FLYAp4V>=~3}!jEvk-s@HxVvCZ6(s{XEN zDY08Y+8wG2~tXjqa4(36dZ?9`}`|E3CTt9ewodzeU+zfuK4t0AX_yPf} z*gM_!xyl+pX<63->)15LEeu17vHu^&&LLQIs7mIz@ zU9WrcPkNT5Dz#^+-^?cOYa5+Iiw_el*C}qDJ zvYmaM`uf$gnKmvd764iHvDj>e-a#3u_S3aWcD7*>opY0s7BrZ z>gmz2On(QxWs_;und53dIP}gHSbCM_8e0lZ;VGv4eX_S=<_;#h=)~sLD%s=ddL#e* z)yv&6!uXH5-G9&H{y);i`aeh)%YPzWEdM8@>qJ{C?qnDd4Y;cwxjBn}-6Q~Z$mAgV zUxVqUx4nqupZuekf?nVW%!3F@k%UO#r!U!Z@AK-2s*UGJBfik5KfB?tVp?70)#9Gs zN(!5r%NIMZ_tVbFncioW-LKcixA)75UT7G8Z_n4yqVgo?Sr`8OKdP(t$6W@W$_2G@ zzqa<*rJ|E>{&!7HcSXgfwzjY9>(buAno4OchS&Q$??^(e#%L_&T`2-cIXlzTZMP8ia!f`_q&|8uk zls)mkK`&=Rtj%iJCr2z0;@jPeJ;?xV9wR~M?gb(nlG>xJhuGDVL;dg=zJqyr#Vg%e z5hTQ>Nv`*XVAsM-d+50aiB6er@+1xSh*EssN7ipL_bfTZ&+mWe;>r|CRqsjf(e(~Q zFh7(DB3IxvS3@iZy--59Y4lI z=P#uRlN*Fu^qn8QhIZN{)uC!@JzpN&S-`&D1Inzit-3E$?sEQf92=3J+_)*`B@@;=hw8tNOEq-%2rS&yX(aLE$aqog1| zxQ=M*)-UV=S!P|+;Ng)IFU_59ovwz7k)*1+bVViJTb^`hH;l&X zMFMv(D2P_L0t7xZG52+;XvWa5kAj65A(U8P$B?f5T}qhGsY(BsD03hekA=V^!@wd$ zE_0G363B$S^}|H}S1KSOefE{|{o2B1m#)v8Cj)~V&lmYj)My((botkJ@N0o;b?qDC zM?wI{_nB{4?_$ebSlVCWi#kp0ZB?+BV7L$l`m5_@Xl?~zr}JWtXPpF|t^Q6M3=|)% z56~xmnb=D$v@_Gwr83?qrf!L*?b*L%GLk5U{mM-ji&m{qLupTlWQB8GL|K_B(j5}6 zu@H7eD52u37@(A$>itZ>sfoU%P~a_#OhgUOp_FI3-p)*xZyKb!QAPl@p-`~ed~Q68 z-)QO|vs-UleN92B6vAPx&})Z9cs7($tw*cG%7;t$6eYt(b0Lo3`xi2lt`%40;G~T# z$#%M&Z*T)n`#M80(M4VhI8lRzQ~;6&4zlOaSqh92gK3+Zdq7M?VXAtxeI8mPa@G+# zkvE^{p_Z=?Pq=u%g2mve;9UFqUxF7d2%^?L*DMNCYD7dLEFhchuQi9!Loxjyccv>C-8=((rSNZR z%EpOV?zQO72|^PgB1Qhzi~??QA5%T@4sAO=Z-eU4~rC7JZz#?DSnwgCv{oCxYt6bZoRQR=iqt zqerPqG2;td1U;Gb_=5Fjxm``(9Bq%)b8$fe1Q9A^h1SMha6-LavYf6_+8)gADZpXI zPz`4Yboi(yDsawDmdR)C(<5_v%2B}H^-hGb9$Qs((CY_`BXLG32todRU_%^)5AlOo zlv#H>oJ1e^A!zn4gLW2M#A31 zGfa)5!=)T>by3hyqLF{UF_DKKii#W`MjjBjjpY#x zRkc@)hHjb)Si5r;pn1Pi5mvHKIb3>28NW}UKF?RQ*ua2S(6BXaTpnsjN!$ilwC43e915h08GAvCp)Q1f#Lh3~@ zLh1rWEo(}Gnt0P?EhTy#LGf^>$kcD!A*V-d>LGZ*QK-{~Zy>I&%-0a!V)(nZ@~i5%aK|~dSWb4+%eHq0E(A1=;meNX= zFQU~Ja{|B`xW7a5358ROfj2>=D0dN!uP&m@j5)b^B}QX(bj#}zzt$aR^c3lM3Ff6_ zjB>=cqK=s#Y|uP#B~ijAe74qw7;t&SuvxrEIyX~h<4MVH zXC0OJ4G7|nSH)f4i!g5zibJ51Id>z;0oW49f#nYXys5o({A+fI60(Bb`&|$3qT1$y zkaLHG#*CcVeL=E%jvNpYVXhKqx;&Mtly>(Ws_n}T&DhkR4&9qbrx(hyyiIgeDO-n2 zV5;Zo&e~CcXAR}hu#M?gt)*fo6v;ZSHS06n@DiM&Tvrn|dKlxb*w~QLEW$pPYz&br zgy~<(i}yOs1FwMK5o-1Sni42})f*KojS8^{{rs)21H(4zTthwu zM)<1gy4R6U2o?B8+Q1kBcy9Hkhqe~mkpAK>j)>7&=c<~95vfTv0g>nQbF`yYFTA*9 z)`(sHdrUO*?^q{vExt2~Arhisq8{$pYup$mckb#++d`r_o4O4^^Ob{!~k-Y#G#N?Jm z@=+^{2I90h`F9*n<{S1JqMPjRCoc)P#pnzM<0+Sxlay0L<7mim|@YrW#64)E* z603Z$H_k+B68Jm?BUWk|kjw_212PNwYb6(1KvE;wDkv(nGxExYMIy2}ckw8ID$Okz zX*bzA|7K1jeS%~K<%Iy?^1{USJ+NOSkHf>#Ln&N6oWOzasOv% zp3RW?T(u-{ta46o6t@CMs!ph5i70?@aw_=Z2oQl~AcHx5P_T4e_b|lk=tE?MQVjt~ zfk?T3g;z|>JHA$pA9OSj6rtiTb9oh{Uj?Or{raP62-x=!M|K59N-l`0j+RSh%~p1t zEdHRdi#+CisNflPK5@}rH(T%A2=27BXfdTW?f@ndsm)|++hcH$CgB0@EefpHW}<(%2yfT-(W}CB{p;w6(t3Nu%96xZ@mZJUOc zlJ=oWxOiH#i(AVHb;)K@Y>{C;+-fL=S9%4nhNwv@RI2RcEJhWAQ& z)A6XI$W$7X_6Dr2X3FGTp!!gRos-^knLpNQ3OUf3q>WKI)!rC))txTk^h2K?r8vgS zTI63|nK3lFJ< zFf2~@6yl4mrX&S+AQ|AF#5%lWHjP5eW>*BQ8s$$>2I^XXUumT$%=y}NV9CA@i>ju- zGk?@1d5_+nI<_(bdP>ZXOuemDTwu;Ik%)a(X%ViFID5b19`2NTrHk+o=NOIt z$YRMwk*;r!LvxQbM6CChg3}EcgXB#Sr`o%c(n#C)TPSM{EhR?$ zLGl6r54?IRq5ij|xx8R^Z#Ya=rYl~34u%y$i?=`OAm|8zR6*g-2106m%GjcB=}w#t z5GdkiS)2sX7luFzgrAU7Yk`n^#`Sa4+(fVT{0zw0i7T19yIL*8jwgSpQGh(TA(CPO#jl>>26HRl9F5)FwNs+%iU;P*!@BNU)Y{|E zoy(qt%g*1v_xm`z+TQm`UBBYu)jV4 z$a3ba|wq<8sC3W&hB1*F5 zKicf#$b|yUX6)OYP;wf3aN+0|l5V{U_IKoB$u}T*=BL!+e9USV6!CBIt;6UU>~*?Th_a@)NH_5 z1OiABS5|=i*B8Sg1d?c;fSmC)XK53dRp2?~*Et_ICX5^p=Azlap3wR%8!e&4HW<7_ zmjrM6zzK`yLPNX{Bf@>nN+xbmB5<}QUU=*B7(<}v^@-XG{8uHN&H1uYM5L+@958B~+S99nsYXaaI)cQ?MVq!nRC5DNZ)Hp8J zO)}HSdRi1N`Muu6-U(}D4>I_LY-9ok@!c72ACrwj#}weq)qpS4sRLa<^)2{*9Wk>& z%suFiRO2G~s|U3Z$P$o^0RSyo^BD^67?j4Nuqo>{Y;1pyN#wvZ&Vw064n_jdT>$U8 z2sG1T&TBabY|sUm(+wwmr96q8;2>=hIInjHkp`7f!O zr|xp(c3L&o?b(Q=1WKjTj}1sxwjhDF%nyPDtGWkVW?yn4KK0`#TvQr2+Y{D45W8Dm zPCn6nNF$UW=|oN}Q;Qm_@XOy%V=6EnI8@F95$7<`ff_(^0uy_VIYTXT&4o%DHp{9z zW=fGvV|5a;FTh=^Nb=n$-^Zs^Bsxru|W<;U$( zOD{(pPl?%hO-~cg=zE0G+;6XgD-EGvkvo;(4T~UHJ$g*~U5_ca4Jj7F0B%fg$F^XU zq`sw>(LNHacH47U&cl!2zuny*dp7iQNWw??DJI;L4$9|DyDz|^MJ-%cid-VlPf-I% z4o5|4ZuA_g?r`Lnl49K{JO|*p(~^~VJ4XChK*CIbjp)DwCl1o#P2ocKebh?Mc)1B^ zjN+qf-)_d5!|f4YEgpM3D5d7%is_xeY_!cYVcv(l~tUyjMI7Qj7pJ{C3ND}kD zr<@(^J*8q90wa`hN@{h45u<@o9e(#Qf!hWxT%_ z&M~Y+?~>iID2i;+eM;1Ili)A_Y?%IJc<>iSDC5jz8lTs_GJQbFRaQaVg+CC~=k zsN*1pA$B(x4_xGVzLbJQ+-e!PevVs{SfM_g$okuucFH+PhH3xnBMzw#FCV`PW2M3n zQBkieP@V;N^Ex4w;6P-1Fx^ntC5})-DELAi@SMbUtli?=a zLou7+0@LGpVg#j-TfiXX)F{Sjg-vaNvLt{E&=2YFei|Bz(n(sj7m7V z7N+Db;n*@UEo87xxPTG>S!q;JK`RY8Ilj?kjtRcg$N*bJv1UZZcH`2ZbF`$+GMupi zvqQVg<<_xx!3a?~`N--dr4>o|;R6ff5Qd-wK|99WRP5)sN_ewrzPFN5q>U&jlvhhZ z1Ck;Zx8OB}p}ohHGS_`;+JrYf!KFcokIokqNUpAD%b{e`C7B?7|K<&}=iMl{Z^+@= zOX`_m);EVoWX_wWTTDnz{z;c{pS7!YEQm^$wpn5bo-;p-c>4Juy~NB(hy9vW+^dqV zzn93==|J>7q!eKjKZ?{L2-p}3U`tqsnH~xM-pCGqZeY$0U^M?Tg$QC1z{ZR174*zX z2U%h5-kqjYm7>jX4PWp6;2}+|1JR+oP6h;*_aJd^!gfWHQbt44caBfUw>s`?C6#b%uw2BBnl^n$ZS}IF2o^-qKIQ$*1q{`+@%!r8lYIoq4S-krSPDqQNHRo!g9d_8n=)$6SpIq~c=IZ+MX8mM= zYyUJSuXC(v@`GkoPvc9p8zce#6L^C;`j4pxv@Tt5>*%SyDsrUE8g8)O zn7`vm+uF7yWefb&Y-b8S_$Q1giden<=IV3oysI@wrd4jZ>a;PNDnq_2K-Nb9ThI0k_m^zC@?``4-6LEh0cIX6B2c2tRpzoA69cl7R;A7MSFHK!6w; z(Gn<&w2NuzrWL{x*GPqz>dE32R@x4UN1}8;9QqbrVMW87)R6u)Cf z9GX8KOJW?u{%BIXj}rHBLP&b@&T%Wm3@l>e@~lZ$>G_qRHA!4*;3a{zyE-?#Fn^foWSmUAV$Un}KU=ZihjD2yHAA2huXVK5( zu6Q4lT^2hu|G>_5c9WXgaKhP8S47Sle??CkSd{!)_{0;7$q73{iuMs$;Oj*pO;=*f z{RP<|5lS(mlMHFx`RiNPcvnIZ<dZbZ9{i1gTxFNSS%O?Cfn*;okmPk z#vg9;AbVSbn1C{3IM|5Cw)|Z%_LQnulT>G}tI#o1c`5Tw7OBiDzTKNanUlAtA#;Pj zxJff~g#$GsjzMVTY%62>V!f8bU`xjk03wb&!urLbWH29LBLZz=2<+Ck8%>8FaeBsL zY@|lNu*^Q2CGVT{c!*6c(*wdex2!WrXstg_Fwj%mTta=ogRtA;f?cB z@*!o3`jpl`IvM54r$*962j}}C?<~;q9(iOJQ{1HNb9g!q*u5V7hc+B%CJBjjNKdd` z2O53|sq0Ti^-BWdl00v;f2gdafm8N$KZ;7w*00No1DMfRMUpf&7ZfDgHCS*%e)1!a z`n_`utT8cLn=OQn_C(i#4NUqh{WECFLmh$hakdl}%JaDE7$KCSk2%et`6r7FxGp$& zwC8Dg9C^s{)gOiVGuW2s1o!0i)zOC_N6cxqDsM!;)x9UkrcAD-F}Y`9O>?e8T4kyg zR&PJ+lUnLU9LUS=2H$%6Ktm~yj)2otwl$ODRI{k$qeyYi4G}EwRkvQE7mP~*n$6QC z>H0T5W$+xvr)xybR-@XNUG1bkS%!dQm)AX?i4JA$Sv-fSeswh!!bP3}o2=vG{1r|C1))ke@%JZff~%p9EWWI~!HIb6!r3d6N2P6K$^XK;3vjDqqZM4aT?o55wND@tIZAbwsdcG`uGab zUG+NLEM%vQZ%1~r&aC>)SlV4k@z42#7JBSSq-U}5s1HgX6Xw2B7}CXqoU*+!Ku^f+2U!Q~wh!S% zxvwMNs<`p<8qW()JegA`x3U~s_cEPg1KiwPYJ#P#Jh`!a$JY^l)GxfBq#ekGkhIyT zm8Z-ZZ74rLK$-`LQi5+ok!JjB8{-aDvEv9EK3bu94G**8KAr_ilKOUo(Owp8*o^8r zJUo8b+KILeuyghE;^v*GY(o3?P6MGiZTVnp_-BhAcbQ zgZp;sH2{7Iw*5-OP5;^=d;9Ki)ntJ??jW;BHnDf9)g&JMMhkd8 z=h|3KU*3TarAaraW3&BShLx_{wp+PKasJVgf;_Reev_)%GM1YHO;tfVDOReWO(5#1 z@@8dRGbu#oYDsDtMF?)zQGf{E-}Hl80(`4YH`wUg=oI}B(Y9Ko%l+oTBB{m6&2yis z8Kt4MJn2;!F@Et}-r{YgtUaABsbwl6W{R`B9Mi2H z`-BD{+1r)6_7NT?$v4b+?NnbbK#kQSdvt;B*bO}9TnAu4{pLXqtxo(#DeRxQJ>&v> zm60om08-tnUy_nCP2j?ponXE$nY$UiBjI$g5+(y#G-XZ(F$VdXJ4|Js1b= zllb(NXM6%V3-z0hypw|Vp|z}ru-mZusM1%X`~G7EP~g0lw4An!%a~EZ0n2;zSC&eK zrhcMP&C0g!$d(Au{I)nque}TQ6lA$}ORlu8y}x}A8$&2Ed@4&&6Zh^nQI~LBvI7AR z+bM+ZeLR)sv_ju)Mk@1N61COSKAx8t^n;z_Au~YD5FsV{I(1zWyAH6ByR2s+~vI-N9;VlHlekDD^-uXa+lHLVy>$ihL->n?`}pQ4*IABJqPC_5bTJ?#D=SF{Wm(D-G<`5==Y5Nk0>>RfDdnza?gF$ zJkeEpgxpfAkJ$PlcOkyz#2V~coY(VT3W)9ENtp1x=N zBmIQQ6r=nQ-v;hfY%!=zeJEjQ3LnB#EvF2rWUAz<^*$Ilzs=qYK}T`R16Cr2e-Q&h zKJ9B;2j(&F7@5Y672;A(J9!1H@;G(arRjs)FG?}q<@WbhO}wy=RhZW^F{wU!ytZRq zo7crUZEOUlkf3T4wzn$xZZX@=99W3@KThvsW@>%~eZRnzI(4c4F&O>tf#-j5U98Lu z9RI_0vHmC5#rl8Zx^}dt=5WLi(PMG9@Ka-A@E@o(fMeZ|UAez+Pve0h0@@9H*as=Y z#l=M>-+xIfJxx3Pc$Au&@^BscdD~h4=;+*4{;a%*>Fil_U47g&zu$h3Y`Gb{ef!Zp z9_@7T#YExv^}Qbqx=woGKLTs}z4^ZVmD(&{rgNFNqxK&hv^+C{N-A|i}#uG zo>0w|ys=lhW!ebeHxXv#1C&VQa7s*?;a@=8nvbZ>r2mpt)>U0S9p{!uYO zDm4DJj|0a-q_4_ABC-K4jUN7(gNw7TZwSO(0$h9Mjm)dI6OS<|eZv#&vd6k}Nc)BA zHIy`+1)_Vfs~`IFB*_|U(WuD|WsJ1u5xeM?|Cj#KBV_`6!&QuJS3#+CJ_wZX*iX!G zzINYYEdeQ5$zk*LRfo!A_x7W6_`WHV+pYKm6Fa1spo;sfUA5SH(}qjP87GOG%=a7_ z;ge;ofq4~3t}Yx=ligcYFylzLYvyN-Q!{2!{1JIJVmOOYIy_nA@-=1VK?4e~#Cs%l zX_sh;q#ayw*eom@bjX~>^ZKyIz@cLr@uZ@HGQt&mqv=tEvv%XVCTizMow^wX zc@3tB6?HLsWIXQ{Bm#U`V^>dh4TqbSl#LMeA|iDa+K;fRS(sbllYiQX_&X}G8v0x zs3WXg0J}F0vMaC^<&0;T$&MEZ)z1gQf~bqkqFpp&%8(T+jwn=*gV{?E0TN9ELhZXC zD!ht9i;rxPdJA{%O*J-DR%^EM9uuU{1-W3jKitV_AuBA(QjDpL?ZRfV;;eeoEmem> zGi}&(G9GWYO`Syv#IxUgOPbKE#n#6nil73_2`v4X?iEyrv^&1q<&@_cui~E~Cv=jZ z@>Cyd={`(F5Eqr*PD-)nov!)6x#znvu6!a-y$1p7-Dz0gOKx{XaWq|T$SbuMyE1ZC zMrSMAYU|DHh%c!?6a#4ffsi!kvQv9#AHG9Zyd z$28>M@E8+4e;7p0O`LX%kzd03U{zk`V^vqWlP1+N&cxUi!kbpY_l>)$-}C_GbXVxy zP4`eLycJI{&+1LKx&9OKmw;o~ulz1ZKc zMzDn|S@caimm0xzn-DOnl2K*{7ykRrN^1rk$d@0Hg>|2HlSj z#$^?$QX$o`xQ2=jyq+?F*TwtNdn2?SH2ZBrAsOYw37*7Q9@UWyl9pY_kaXsqSg;8S z4LW}O#TsBWM-5K5^{=N^X{nK~B~<1xH}oKkD&H{yA=L>erk8MQcg!-Wa>}n<84Y zjVq-?VFxtK|A86oJhq>38F|+jN>;(=3dv8u#cima8%!DD%wuTK8i2k}g?y98^c_rF z3*Y~1uU7MR0VeC(>turq$^mlMhAOHdfW6 ze^TCbT`@7bZdnJwD2EYseN;UmyqcTfEW$)mi;D;-#8%@-KVQT{CQli%ZhkDSD`k-W`>44EK_vW&HMm#+1kzMic7!}gzWvI;=>>OVVwR)q+sXV#g`3Z3_-Og{pEAZl8l$Y!Q^}acLoaMpYKXk|7GsrMzU+{}Cy(AMn|m;V zThD~rIVQB*nnth%M?^yK;~!f%qB@)Sgzt^JD0o(cm^n7s&bed(-U@Jy!>BUez!=Z<+-V)zi8SgkWLtp|x?izjznQ zaZWIYk|A_{$Ql*9$5T-)Lb}KP3ZY*CKP6qzKq2r%>s&K*`}!fEB*TX3Lh zAYs`3Vcq|P6~zD^_*V{6A?e1;5eicq0=xi`0jMqll=-74%V1tHW&+hqKEK4Xw$J3$ zw?n9B8>d74i(>5^0x9t^f&=FXxVr+C4MzZmuM+4B5`-gzIxH&rp^f5M1@U+oYRsd+ zkOO*y53%;{5(PR4gGgQbGPrwKlC*^muCI?Ylobu9m+iMim;&fjKW05F-6+ z9qSnD6#8gMI=}C6@+JuYWbsu@h6U;a-tZ;22v}yEX5PWqd)L`@o1>O>e>> zN~yVo{7R6eNlM6wA|GNA^hp)_>II&~e^#;0UehXGyz$Q%$f{o3R;> z_G@e^s&8@c7w90KA@4sw68?vtU}j+aU!O1Af6^0d|0jClMrS9Eqy-T@?dXUNf`h|x z1EDbCu^+}f^R-Rp5hX-8@)2m0eLn)ESX?;p>sN8|+aS%Wa&)Jur>duGwYS-`>&CG| z`=zQbH_y$6_@y?+?`Y(WPsit{W$XLBuj=;o_Nosaz3gOLsZHqOomfw|wb1cek#lK_V zD-iTG|5e_KEN$70`d8j6vM5&2JnFK;Gd^mbxBNxhUXHO2+2jj&>xmbVeG?|mu|LmwI|@Q-CtFO{Je0s6*lDLT z^UvsfV0ssSc4Pl-|9m{XtfIzzmIXV&ZbBZI%-Q~aysN^e+ny`6aaq?m!@l_|BLXcV zEs~a@gzFWo5CT9T+MPtdP`>wr)Uj%57DX%S1_{TZZ@R^?ZoQAN>pskpxFeqXiRhiY zMw6)#x*(p{FVz@8Zjy}Vj=7Eko+1DQF0>sv*M~;*v{YI8(!ZGH=Q0Ka_PE;xK&|j! z{^O$#0F8g|o#O>;x}yht9O-qABPpdbR1XCjL?O|a^Yg5&qo^%KHU3S43UtU zTjQh#L`M(vaYvLkGV(acj0NWjvZtH*CT>UuP?*`GMGny)3{Jp0hKGL8iW{A5?Z{00 ztAgHG4-kBhC=#|40TfD#Kxa9aClCw2zVw?9BqpB=I2zHSy3*=?*R%9EB4KVEPZ$ya zw5I7c`Qg~eki1lOSVq%yu@uH~uW7jCE%mkPhIjyW=%;^5*_|^rpxr?hf?5+0K^Goi zN*_k%JsdS}d3npxXyO;Y7iKe&P>R$f2YQBhARLXszVm2G)ayA;SiS%nD7 zaiwF@Tu60k>rKv6WP#|op>2ua#<2b6f+XM7CjoK@0<|9woY0pgDN5eWZ^1-zFF*fj)-#tM-bJYJmn@+I9<#ln90jRc(sAID{5%G+$ z!oVJDE>S_EX7H`r6|90Ymg`IdXH4qETo=_S4vRo)<>n*z1*d31qWI4c^Wi!yfygI< zk0>++C5p6&@1{NamlO;JQ_Bcc(t9oe26FUi3Cx!o?Tbn1kCC%;R}Lr#6a*V)k}GBS z%q>e`%0`nYz~eA3J_Aj7wX!a}l+&aph?K>C)Yl79XAt)<;xw8*d&~<96-#Bc%*8BKG{Z%{3@pBUEdV|-C|!kXVcToM`F!iBZ1Yk3j?2w@26P3ewl5yOyda2N zWrqnN6N&r^Zm9x8&pF&6(4jB}jzI^zg^pNI zZV`k-1P>FWy{|u}dEWf31c#2lRdD_aLQ@e)h54Csu-dF=$cXEHXIicQAqmw<5QTb8 z`M#v?e%OKybTneUdfWn4O$-K^*`y^`3d|Brrhr7U5M{pCAr7HP4b^ZK7;bi*!V6S7 zH+0PaK2CkU5cJ%IkPy7UQ$5_zp=ejWNPKnR`5Sf-q^Ff?89I*tK2EpTQ`jyS7p9X8 zX4o??_TSCHUaD0oTNiEf*nYcynE7h|ch-i8VGJEy0uxBZ1 z-IM5Uaqso9;c>2lB&Ki#VGZ5juji@4$+Jd|am8&9{=ip&Gi%~O(2442VJNjJvQZ!5 zFyMt;5fl-0zQOqT!ne#waYkN7jrbOtg}gl>Uz}LJ=TS8lLhQZ82<&7LKJMv%9fZ@H zJuWhfu}n&5Fb=nEj-fZM&6WXHr;Rm!dNHnKG%kCSiI0DoOkQ)7?EZ?0hz!VGN^ zI>L<0>-qL7V-klF-4#e6OaJ07$3R&fq)}e$kDiu)i4r`*DDH9K)^U0W-9DOfsnPa# z9JO9KB^}u_0b((=h24l$d%- z&#`qmO$Oh*G`?w=EA$rSV3L7l$APNG{mV!@vm&&y-dRfC`1tCyLpZC`YaM@h z`(^SAtq28(uO!=wvG787yPY+7!ET|Qwf9}%Y$-shEZuzcGDb_xz9R+I71o0T*m&T? zSgXxmEd^Iuf*$&9QE;r;RwrHc_(?%YC(NC%%nKSfQ+RVbO#4 zr-iFSvYz{b1uL?-Rkha3?&Q+@SW2s7+;O%h%j)X1(}q3GfMUC|15cJ6y~MB8w)Vd- zZ+3S4J9K%{Y`Z+AqS)>1xb4ca3K3RS{DnW%3i`JU^};l%X}81$`w-Wx9V?@Y+zbP?;l zr9I;+&~O>OGNUT%!^Hgb6YTqPNV+KV$W;6Y-G|QxS$|Hd9tnvjMlGHr81SER){4q8bkG9b&7|>S%o7-9#Dv?q$0IhW_D9CP(CkA@ zWV+-F5Sb6!IV%WoH+Qz!71st++4i~{(eiBFiDF(%x-G5q9El3rVe{~{|> z;VigIvx_fX8Izc$TZBgofU~gFWu!KfJHv#B{$6MzbH`limC-T*0I5#Z(U)5ItUEI3lo?!pVw@ZK~S)rVv%w0D6OF;Gy4LNomK z6vGHEWZ`z?(bIolVPK+`?__|2?5U@d3p)su8a%Ht`_RcrC*b)-cqmiijhWN;5jv@8 zRUZqj(gUsYJZl#97_oi>Czv!t25whzgSfOC91yS+yBA7Be&LC=Bc0Sfb9E!Ci#KH9 zPe>po#O2Ot(NxTMKfP&4?NX0y9omyt-i+L5^a2MT_AjS==#(Wn-&4mqGG!Ff zniI>j31s?KC0g%lU*9lpQ}c@`=Bed${13!b$p>w>ffR^oNe*YstkXY-Y`}`$tFjbW z8p_u8O@YZPfv8{_uI3_iaf&{hNXT{q>PgwarR>c7E$g7sb6LK+O73ZIsT(l*wR7N5 z$`4~<`+k`_zOZ20E{F1(fLc9q!$=nQ1!=fMKjwxc9bvRyjxzyLo^$nD$utG=AzxX8 zFunodxr;n@mAKp#fQN&470>_U?JUFc*0z0*YjH1L+?^LI?i6=-cXxLw?i6=-D-K19 zySo&3_se3Rbx!YD>+WZNx$6sg-prAaKffe%j3hH-dVYUg-tu$@wXkU+7jSEy@CLhu zm2_B(OrpFxV1aJSBUV0)wCIiOe@v*(8>|Hh6HRZg{+00)1pp|WS(6vUX<-Ml^jwE5F5xo{RU*y@um0TpjE z#~Qn~6g!_u1X9*dLnC3M-A!WtNt_jD)sF8EJmih5IDff?f4?>V<2@e}`~TEkW&P`W zKGy%}J>Q6?Y7}w5Eky8kEGMgISg`0~0ew*9apLFRK~9Mpa1~age=6kYD7URR=` z@Y^3&o;F%|R$lK{|9IWk2YGjou*Us#6WU%h-7^mNvaug^YxV^C+~N&Ya_aVYc6Izb zR|pvO+;syaPd#2y+<3l~neneZR5<<0<9XS!E3(qN?*F;X*{g0J?oevY!`aIrT}gIf zN;@52aSlQ0aO!YZWD4B)%I4vyjH3{8SK4}V4N~B>YD#43T0B#W^HHj~E8}Fepw`p6 z#Cj^+^C5YankoQ8XR?ERDkK}fd!NRwh|@fmyG=58a&J~5w>^P-sP#WYiE%@P}4)@^c8uFNf8O z-?&E*s3s+Xp~n!ab|1Gq7KI&Qfz2`_3hgTv7NPg7=s%-z-g1s*<+N+z8%7;YncZ;kux~Sbmq&*&4xUIz8 zk>TAxl@el;m0K8J<;m=|NWaQ2Rft$RJUkL%^~R)lN%TObPEAEe8H|L8itUyKD^~B3 zPazSuC`kp(~sl~^#sE#7fQV-dkk-ENjT zUpWx)Bd<(-s59Go7ELc=mVJ(Fw-&%37PViQD9*Dd`lM<|7J2NJsW6CbI=P4Y(>S$G zIZP!$`)y9H_E#wma}n61k$oY&;pR!7hz$u0aM`nMJ;YXPT zb7QcHYw zEA5KT6Q*X7q5y;#*LXCzwK+fKJcn)1R*p1r))J%6>DjCf4|khBh4!{&R-(iAb=Nbu z3}bLUqr=a1B^Il-2ld-lxByinDDzjYO1~RHDdD<+CzyJ(fi&I6m6^pH+i^7Hl&jED z(Hrwn1YJW|LM6RLBd$E^NVv2TpAembN_ipC3TIcXq}gED4T`oNVgehA2ZVvev(Y

4@>v1w36_oLNea#8y!j4C+E~E9+-ZAP?+%g0c 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�|m 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 IB1iat0Lw>3Z~y=R From 1cf2fc78704c751de79063b0512d844c051ceb02 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 11:22:10 +1100 Subject: [PATCH 135/249] updates sln docs and removes pdf test bits --- src/Umbraco.Tests/Umbraco.Tests.csproj | 4 -- .../UmbracoExamine/TestFiles.Designer.cs | 42 +----------------- .../UmbracoExamine/TestFiles.resx | 12 ----- .../TestFiles/Converting_file_to_PDF.pdf | Bin 118488 -> 0 bytes .../UmbracoExamine/TestFiles/PDFStandards.PDF | Bin 54932 -> 0 bytes .../TestFiles/SurviorFlipCup.pdf | Bin 84177 -> 0 bytes .../TestFiles/windows-vista.pdf | Bin 99838 -> 0 bytes src/umbraco.sln | 6 +-- 8 files changed, 4 insertions(+), 60 deletions(-) delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/SurviorFlipCup.pdf delete mode 100644 src/Umbraco.Tests/UmbracoExamine/TestFiles/windows-vista.pdf diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 983a9a4780..ef316f9e1a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -567,14 +567,10 @@ - - - Designer - Always diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs b/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs index 79e2fa8c53..ce86c80041 100644 --- a/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/TestFiles.Designer.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.18034 +// Runtime Version:4.0.30319.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -60,16 +60,6 @@ namespace Umbraco.Tests.UmbracoExamine { } } - ///

- /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] Converting_file_to_PDF { - get { - object obj = ResourceManager.GetObject("Converting_file_to_PDF", resourceCulture); - return ((byte[])(obj)); - } - } - ///
/// /// - public static IAppBuilder UseUmbracoBackAuthentication(this IAppBuilder app) + public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app) { if (app == null) throw new ArgumentNullException("app"); @@ -60,21 +62,30 @@ namespace Umbraco.Web.Security.Identity GlobalSettings.UseSSL, GlobalSettings.Path) { - //Provider = new CookieAuthenticationProvider - //{ - // // Enables the application to validate the security stamp when the user - // // logs in. This is a security feature which is used when you - // // change a password or add an external login to your account. - // OnValidateIdentity = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) - //} + Provider = new CookieAuthenticationProvider + { + //// Enables the application to validate the security stamp when the user + //// logs in. This is a security feature which is used when you + //// change a password or add an external login to your account. + //OnValidateIdentity = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + } }); return app; } + public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app) + { + if (app == null) throw new ArgumentNullException("app"); + + app.UseExternalSignInCookie("UmbracoExternalCookie"); + + return app; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs index fac130f0ca..c2c4810564 100644 --- a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs +++ b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Claims; using System.Web.Security; using Microsoft.Owin.Security; using Newtonsoft.Json; @@ -12,16 +13,18 @@ namespace Umbraco.Web.Security.Identity internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat { private readonly int _loginTimeoutMinutes; + private readonly string _cookiePath; - public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes) + public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes, string cookiePath) { _loginTimeoutMinutes = loginTimeoutMinutes; + _cookiePath = cookiePath; } public string Protect(AuthenticationTicket data) { - //TODO: Where to get the user data? - //var userDataString = JsonConvert.SerializeObject(userdata); + var backofficeIdentity = (UmbracoBackOfficeIdentity)data.Identity; + var userDataString = JsonConvert.SerializeObject(backofficeIdentity.UserData); var ticket = new FormsAuthenticationTicket( 5, @@ -29,8 +32,8 @@ namespace Umbraco.Web.Security.Identity data.Properties.IssuedUtc.HasValue ? data.Properties.IssuedUtc.Value.LocalDateTime : DateTime.Now, data.Properties.ExpiresUtc.HasValue ? data.Properties.ExpiresUtc.Value.LocalDateTime : DateTime.Now.AddMinutes(_loginTimeoutMinutes), data.Properties.IsPersistent, - "", //User data here!! This will come from the identity - "/" + userDataString, + _cookiePath ); return FormsAuthentication.Encrypt(ticket); @@ -51,12 +54,14 @@ namespace Umbraco.Web.Security.Identity var identity = new UmbracoBackOfficeIdentity(decrypt); - return new AuthenticationTicket(identity, new AuthenticationProperties + var ticket = new AuthenticationTicket(identity, new AuthenticationProperties { ExpiresUtc = decrypt.Expiration.ToUniversalTime(), IssuedUtc = decrypt.IssueDate.ToUniversalTime(), IsPersistent = decrypt.IsPersistent }); + + return ticket; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs index ac77a49db3..41d52e0684 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Security.Identity ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl, - string umbracoPath, + string cookiePath, bool useLegacyFormsAuthDataFormat = true) { AuthenticationType = "UmbracoBackOffice"; @@ -30,7 +30,7 @@ namespace Umbraco.Web.Security.Identity if (useLegacyFormsAuthDataFormat) { //If this is not explicitly set it will fall back to the default automatically - TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes, cookiePath); } CookieDomain = securitySection.AuthCookieDomain; @@ -39,7 +39,7 @@ namespace Umbraco.Web.Security.Identity CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; //Ensure the cookie path is set so that it isn't transmitted for anything apart from requests to the back office - CookiePath = umbracoPath.EnsureStartsWith('/'); + CookiePath = cookiePath.EnsureStartsWith('/'); } } diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 8f828418f3..6acdd4b4ca 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -14,6 +14,10 @@ + + + + \ No newline at end of file diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index b77bae14a4..267e89a2dd 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -14,6 +14,10 @@ + + + + \ No newline at end of file From d4b21243ca9aaf0940e8511a2220cb49a4f795e3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Feb 2015 12:14:59 +1100 Subject: [PATCH 141/249] Allows external logins to be listed on login page, updates BackOfficeController with actions for invoking them. --- .../Security/AuthenticationExtensions.cs | 6 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Core/packages.config | 2 +- .../views/common/dialogs/login.controller.js | 6 +- .../src/views/common/dialogs/login.html | 42 +++++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 10 +- src/Umbraco.Web.UI/packages.config | 3 +- .../umbraco/Views/Default.cshtml | 31 +++-- .../Editors/BackOfficeController.cs | 107 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 11 +- src/Umbraco.Web/packages.config | 3 +- 11 files changed, 191 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 008f24f492..dd688cfe25 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -323,12 +323,12 @@ namespace Umbraco.Core.Security private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { - var allKeys = new List(); + var asDictionary = new Dictionary(); for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { - allKeys.Add(http.Request.Cookies.Keys.Get(i)); + var key = http.Request.Cookies.Keys.Get(i); + asDictionary[key] = http.Request.Cookies[key].Value; } - var asDictionary = allKeys.ToDictionary(key => key, key => http.Request.Cookies[key].Value); //get the ticket try diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c5da56af3d..f0f09c499c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -63,8 +63,9 @@ ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + False + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 559eae83c4..93c9737158 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -13,7 +13,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index c9e7adabe2..00d3cda1ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -16,7 +16,11 @@ }); // weekday[d.getDay()]; $scope.errorMsg = ""; - + + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + + $scope.externalLogins = Umbraco.Sys.ServerVariables.externalLogins; + $scope.loginSubmit = function (login, password) { //if the login and password are not empty we need to automatically diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 865dd8303f..5b73743772 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -1,4 +1,4 @@ -
+
@@ -8,19 +8,37 @@ Log in below

-
- -
+ +
+ +
-
- -
+
+ +
- + + +
+
{{errorMsg}}
+
+ + +

+ +
+ +
+ + + +
+
+ -
-
{{errorMsg}}
-
- \ No newline at end of file +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fb07ae1ff4..0cec068f48 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -162,15 +162,20 @@ False ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll
- + + False ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + True ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + ..\packages\Microsoft.Owin.Security.Google.3.0.0\lib\net45\Microsoft.Owin.Security.Google.dll + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -202,6 +207,7 @@ ..\packages\Owin.1.0\lib\net40\Owin.dll + True System diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 150759f582..5ed2c4b1d0 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -25,8 +25,9 @@ - + + diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index 308f483af2..8369b5b677 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -3,6 +3,9 @@ @using Umbraco.Core @using ClientDependency.Core @using ClientDependency.Core.Mvc +@using Microsoft.Owin.Security +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq @using Umbraco.Core.IO @using Umbraco.Web @using Umbraco.Web.Editors @@ -23,17 +26,17 @@ Umbraco - + @{ Html.RequiresCss("assets/css/umbraco.css", "Umbraco");} @{ Html.RequiresCss("tree/treeicons.css", "UmbracoClient");} @Html.RenderCssHere( new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco)), new BasicPath("UmbracoClient", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))) - + -
+
@@ -47,18 +50,28 @@ + @{ + var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Select(p => new {authType = p.AuthenticationType, caption = p.Caption, + //TODO: Need to see if this exposes any sensitive data! + properties = p.Properties}) + .ToArray(); + } + @* These are the bare minimal server variables that are required for the application to start without being authenticated, we will load the rest of the server vars after the user is authenticated. - *@ + *@ - + @{ var isDebug = false; if (Request.RawUrl.IndexOf('?') >= 0) @@ -78,9 +91,9 @@ if (attempt && attempt.Result) { isDebug = true; - } + } } - + } @if (isDebug) { diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 46028bff35..e844d8f5fb 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -5,9 +5,13 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; using dotless.Core.Parser.Tree; +using Microsoft.AspNet.Identity; +using Microsoft.Owin; +using Microsoft.Owin.Security; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; @@ -27,6 +31,9 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Web.Models; using Umbraco.Web.WebServices; using Umbraco.Web.WebApi.Filters; +using System.Web; +using AutoMapper; +using Umbraco.Core.Security; namespace Umbraco.Web.Editors { @@ -37,12 +44,18 @@ namespace Umbraco.Web.Editors [DisableClientCache] public class BackOfficeController : UmbracoController { + protected IOwinContext OwinContext + { + get { return Request.GetOwinContext(); } + } + /// /// Render the default view /// /// public ActionResult Default() { + ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } @@ -359,6 +372,62 @@ namespace Umbraco.Web.Editors return JavaScript(ServerVariablesParser.Parse(result)); } + [HttpPost] + [AllowAnonymous] + public ActionResult ExternalLogin(string provider, string returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = GlobalSettings.Path; + } + + // Request a redirect to the external login provider + return new ChallengeResult(provider, + Url.Action("ExternalLoginCallback", "BackOffice", new + { + area = GlobalSettings.UmbracoMvcArea, + ReturnUrl = returnUrl + })); + } + + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync(); + if (loginInfo == null) + { + //go home, invalid callback + return RedirectToLocal(returnUrl); + } + + //// Sign in the user with this external login provider if the user already has a login + //var user = await UserManager<>.FindAsync(loginInfo.Login); + //if (user != null) + //{ + // await SignInAsync(user, isPersistent: false); + // return RedirectToLocal(returnUrl); + //} + //else + //{ + // // If the user does not have an account, then prompt the user to create an account + // ViewBag.ReturnUrl = returnUrl; + // ViewBag.LoginProvider = loginInfo.Login.LoginProvider; + + // return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email }); + //} + + //TODO: until we make a user thingy and make this correctly , we'll just see if this works + + var user = Security.GetBackOfficeUser(loginInfo.DefaultUserName); + + var ticket = UmbracoContext.Security.PerformLogin(user); + HttpContext.AuthenticateCurrentRequest(ticket, false); + + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); + } + + /// /// Returns the server variables regarding the application state /// @@ -505,5 +574,43 @@ namespace Umbraco.Web.Editors JsUrl } + private ActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + return Redirect("/"); + } + + // Used for XSRF protection when adding external logins + private const string XsrfKey = "XsrfId"; + + private class ChallengeResult : HttpUnauthorizedResult + { + public ChallengeResult(string provider, string redirectUri, string userId = null) + { + LoginProvider = provider; + RedirectUri = redirectUri; + UserId = userId; + } + + private string LoginProvider { get; set; } + private string RedirectUri { get; set; } + private string UserId { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + //Ensure the forms auth module doesn't do a redirect! + context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; + + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; + if (UserId != null) + { + properties.Dictionary[XsrfKey] = UserId; + } + context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); + } + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5be8656b13..8520557feb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -142,8 +142,12 @@ False ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + False + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll @@ -167,7 +171,8 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - + + False ..\packages\Owin.1.0\lib\net40\Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 8506c1de5b..877034b4e5 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -20,7 +20,8 @@ - + + From 8c51e8bad8a428056b6009386ea9b7349561f471 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Feb 2015 17:37:21 +1100 Subject: [PATCH 142/249] Implements IExternalLoginService and the db table, implements more logic to start enabling this to work in the back office, need to implement the rest of the userstore and then implement a way once logged in to the back office to allow users to link their accounts with external logins. Currently if an external login is detected during startup and it has not been linked we'll throw an exception. Very very close to making this all work nicely. --- src/Umbraco.Core/Constants-Web.cs | 2 + .../Models/Identity/BackOfficeIdentityUser.cs | 60 +++++ .../Models/Identity/IIdentityUserLogin.cs | 25 ++ .../Models/Identity/IdentityUser.cs | 122 +++++++++ .../Models/Identity/IdentityUserClaim.cs | 38 +++ .../Models/Identity/IdentityUserLogin.cs | 39 +++ .../Models/Identity/IdentityUserRole.cs | 26 ++ .../Models/Rdbms/ExternalLoginDto.cs | 35 +++ .../Factories/ExternalLoginFactory.cs | 32 +++ .../Initial/DatabaseSchemaCreation.cs | 4 +- .../AddExternalLoginsTable.cs | 28 ++ .../Repositories/ExternalLoginRepository.cs | 182 +++++++++++++ .../Interfaces/IExternalLoginRepository.cs | 12 + .../Repositories/UserRepository.cs | 3 +- .../Persistence/RepositoryFactory.cs | 7 + .../Security/AuthenticationExtensions.cs | 6 +- .../BackOfficeClaimsIdentityFactory.cs | 35 +++ .../Security/BackOfficeUserManager.cs | 157 +++++++++++ .../Security/BackOfficeUserStore.cs | 251 ++++++++++++++++++ .../Security/MembershipProviderBase.cs | 13 + .../Security/MembershipProviderExtensions.cs | 12 +- .../Security/UmbracoBackOfficeIdentity.cs | 61 ++++- .../Security/UmbracoMembershipProviderBase.cs | 2 + .../Services/ExternalLoginService.cs | 78 ++++++ .../Services/IExternalLoginService.cs | 40 +++ src/Umbraco.Core/Services/IFileService.cs | 1 + src/Umbraco.Core/Services/ServiceContext.cs | 12 +- src/Umbraco.Core/Umbraco.Core.csproj | 21 +- src/Umbraco.Core/packages.config | 4 +- .../views/common/dialogs/login.controller.js | 5 +- .../src/views/common/dialogs/login.html | 4 +- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 15 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 10 +- src/Umbraco.Web.UI/packages.config | 4 +- .../umbraco/Views/Default.cshtml | 28 +- src/Umbraco.Web/BaseRest/BaseRestHandler.cs | 1 + .../Editors/BackOfficeController.cs | 98 ++++--- src/Umbraco.Web/IUmbracoContextAccessor.cs | 13 + .../Security/Identity/AppBuilderExtensions.cs | 57 ++-- .../Identity/BackOfficeCookieManager.cs | 45 ++++ .../FormsAuthenticationSecureDataFormat.cs | 15 +- ...coBackOfficeCookieAuthenticationOptions.cs | 13 +- .../SingletonUmbracoContextAccessor.cs | 10 + src/Umbraco.Web/Umbraco.Web.csproj | 13 +- src/Umbraco.Web/UmbracoContext.cs | 1 - src/Umbraco.Web/packages.config | 4 +- 46 files changed, 1497 insertions(+), 147 deletions(-) create mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs create mode 100644 src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUser.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserRole.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs create mode 100644 src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeUserManager.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeUserStore.cs create mode 100644 src/Umbraco.Core/Services/ExternalLoginService.cs create mode 100644 src/Umbraco.Core/Services/IExternalLoginService.cs create mode 100644 src/Umbraco.Web/IUmbracoContextAccessor.cs create mode 100644 src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs create mode 100644 src/Umbraco.Web/SingletonUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6c2df14c3..e2f2841dc3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -22,6 +22,8 @@ public static class Security { + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs new file mode 100644 index 0000000000..087f5913e6 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Umbraco.Core.Models.Identity +{ + public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> + { + /// + /// Gets/sets the user's real name + /// + public string Name { get; set; } + public int StartContentNode { get; set; } + public int StartMediaNode { get; set; } + public string[] AllowedApplications { get; set; } + public string Culture { get; set; } + + /// + /// Overridden to make the retrieval lazy + /// + public override ICollection Logins + { + get + { + if (_getLogins != null && _getLogins.IsValueCreated == false) + { + _logins = new ObservableCollection(); + foreach (var l in _getLogins.Value) + { + _logins.Add(l); + } + //now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + } + return _logins; + } + } + + public bool LoginsChanged { get; private set; } + + void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + LoginsChanged = true; + } + + private ObservableCollection _logins; + private Lazy> _getLogins; + + /// + /// Used to set a lazy call back to populate the user's Login list + /// + /// + public void SetLoginsCallback(Lazy> callback) + { + if (callback == null) throw new ArgumentNullException("callback"); + _getLogins = callback; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs new file mode 100644 index 0000000000..c95722f4e3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + public interface IIdentityUserLogin : IAggregateRoot, IRememberBeingDirty, ICanBeDirty + { + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs new file mode 100644 index 0000000000..09306bb1f0 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Default IUser implementation + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUser : IUser + where TLogin : IIdentityUserLogin + //NOTE: Making our role id a string + where TRole : IdentityUserRole + where TClaim : IdentityUserClaim + { + /// + /// Email + /// + /// + public virtual string Email { get; set; } + + /// + /// True if the email is confirmed, default is false + /// + /// + public virtual bool EmailConfirmed { get; set; } + + /// + /// The salted/hashed form of the user password + /// + /// + public virtual string PasswordHash { get; set; } + + /// + /// A random value that should change whenever a users credentials have changed (password changed, login removed) + /// + /// + public virtual string SecurityStamp { get; set; } + + /// + /// PhoneNumber for the user + /// + /// + public virtual string PhoneNumber { get; set; } + + /// + /// True if the phone number is confirmed, default is false + /// + /// + public virtual bool PhoneNumberConfirmed { get; set; } + + /// + /// Is two factor enabled for the user + /// + /// + public virtual bool TwoFactorEnabled { get; set; } + + /// + /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// + /// + public virtual DateTime? LockoutEndDateUtc { get; set; } + + /// + /// Is lockout enabled for this user + /// + /// + public virtual bool LockoutEnabled { get; set; } + + /// + /// Used to record failures for the purposes of lockout + /// + /// + public virtual int AccessFailedCount { get; set; } + + /// + /// Navigation property for user roles + /// + /// + public virtual ICollection Roles { get; private set; } + + /// + /// Navigation property for user claims + /// + /// + public virtual ICollection Claims { get; private set; } + + /// + /// Navigation property for user logins + /// + /// + public virtual ICollection Logins { get; private set; } + + /// + /// User ID (Primary Key) + /// + /// + public virtual TKey Id { get; set; } + + /// + /// User name + /// + /// + public virtual string UserName { get; set; } + + /// + /// Constructor + /// + /// + public IdentityUser() + { + this.Claims = new List(); + this.Roles = new List(); + this.Logins = new List(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs new file mode 100644 index 0000000000..832438f3c3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents one specific user claim + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserClaim + { + /// + /// Primary key + /// + /// + public virtual int Id { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// Claim type + /// + /// + public virtual string ClaimType { get; set; } + + /// + /// Claim value + /// + /// + public virtual string ClaimValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs new file mode 100644 index 0000000000..f9c77031e9 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -0,0 +1,39 @@ +using System; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Entity type for a user's login (i.e. facebook, google) + /// + /// + public class IdentityUserLogin : Entity, IIdentityUserLogin + { + public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate) + { + Id = id; + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + CreateDate = createDate; + } + + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + public string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + public string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs new file mode 100644 index 0000000000..5c68a97d86 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents a user belonging to a role + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserRole + { + /// + /// UserId for the user that is in the role + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// RoleId for the role + /// + /// + public virtual TKey RoleId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs new file mode 100644 index 0000000000..c94ee1193f --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs @@ -0,0 +1,35 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoExternalLogin")] + [ExplicitColumns] + [PrimaryKey("externalLoginId")] + internal class ExternalLoginDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")] + public int Id { get; set; } + + [Column("userId")] + public int UserId { get; set; } + + [Column("loginProvider")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string LoginProvider { get; set; } + + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ProviderKey { get; set; } + + [Column("createDate")] + [Constraint(Default = "getdate()")] + public DateTime CreateDate { get; set; } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs new file mode 100644 index 0000000000..4882df3202 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs @@ -0,0 +1,32 @@ +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal class ExternalLoginFactory + { + public IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; + } + + public ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto + { + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserId = entity.UserId + }; + + return dto; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index b77e0843ea..25d5f2a9af 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -81,7 +81,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, - {42, typeof (AccessRuleDto)} + {42, typeof (AccessRuleDto)}, + + {43, typeof (ExternalLoginDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs new file mode 100644 index 0000000000..b29aff9048 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 9, GlobalSettings.UmbracoMigrationName)] + public class AddExternalLoginsTable : MigrationBase + { + public override void Up() + { + //Don't exeucte if the table is already there + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoExternalLogin")) return; + + Create.Table("umbracoExternalLogin") + .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoExternalLogin") + .WithColumn("userId").AsInt32().NotNullable().ForeignKey("FK_umbracoExternalLogin_umbracoUser_id", "umbracoUser", "id") + .WithColumn("loginProvider").AsString(4000).NotNullable() + .WithColumn("providerKey").AsString(4000).NotNullable() + .WithColumn("createDate").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs new file mode 100644 index 0000000000..bafe30d901 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class ExternalLoginRepository : PetaPocoRepositoryBase, IExternalLoginRepository + { + public ExternalLoginRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + } + + public void DeleteUserLogins(int memberId) + { + using (var t = Database.GetTransaction()) + { + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + t.Complete(); + } + } + + public void SaveUserLogins(int memberId, IEnumerable logins) + { + using (var t = Database.GetTransaction()) + { + //clear out logins for member + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + //add them all + foreach (var l in logins) + { + Database.Insert(new ExternalLoginDto + { + LoginProvider = l.LoginProvider, + ProviderKey = l.ProviderKey, + UserId = memberId, + CreateDate = DateTime.Now + }); + } + + t.Complete(); + } + } + + protected override IIdentityUserLogin PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + var macroDto = Database.Fetch(sql).FirstOrDefault(); + if (macroDto == null) + return null; + + var factory = new ExternalLoginFactory(); + var entity = factory.BuildEntity(macroDto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + return entity; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + if (ids.Any()) + { + return PerformGetAllOnIds(ids); + } + + var sql = GetBaseQuery(false); + + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray();// we don't want to re-iterate again! + } + + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) yield break; + foreach (var id in ids) + { + yield return Get(id); + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + var factory = new ExternalLoginFactory(); + foreach (var entity in dtos.Select(factory.BuildEntity)) + { + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + yield return entity; + } + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + yield return Get(dto.Id); + } + } + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + if (isCount) + { + sql.Select("COUNT(*)").From(SqlSyntax); + } + else + { + sql.Select("*").From(SqlSyntax); + } + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoExternalLogin.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoExternalLogin WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(IIdentityUserLogin entity) + { + ((Entity)entity).AddingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + ((Entity)entity).UpdatingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs new file mode 100644 index 0000000000..17fc153980 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IExternalLoginRepository : IRepositoryQueryable + { + void SaveUserLogins(int memberId, IEnumerable logins); + void DeleteUserLogins(int memberId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index fb66dc7ade..d13df47b71 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -122,7 +122,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", "DELETE FROM umbracoUserLogins WHERE userID = @Id", "DELETE FROM umbracoUser2app WHERE " + SqlSyntax.GetQuotedColumnName("user") + "=@Id", - "DELETE FROM umbracoUser WHERE id = @Id" + "DELETE FROM umbracoUser WHERE id = @Id", + "DELETE FROM umbracoExternalLogin WHERE id = @Id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index be6d35ca27..356e876c54 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -64,6 +64,13 @@ namespace Umbraco.Core.Persistence #endregion + public virtual IExternalLoginRepository CreateExternalLoginRepository(IDatabaseUnitOfWork uow) + { + return new ExternalLoginRepository(uow, + _cacheHelper, + _logger, _sqlSyntax); + } + public virtual IPublicAccessRepository CreatePublicAccessRepository(IDatabaseUnitOfWork uow) { return new PublicAccessRepository(uow, diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index dd688cfe25..e71dd0e00c 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -216,7 +216,6 @@ namespace Umbraco.Core.Security GlobalSettings.TimeOutInMinutes, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440, - "/umbraco", UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain); } @@ -420,7 +419,6 @@ namespace Umbraco.Core.Security string userData, int loginTimeoutMins, int minutesPersisted, - string cookiePath, string cookieName, string cookieDomain) { @@ -433,7 +431,7 @@ namespace Umbraco.Core.Security DateTime.Now.AddMinutes(loginTimeoutMins), true, userData, - cookiePath + "/" ); // Encrypt the cookie using the machine key for secure transport @@ -444,7 +442,7 @@ namespace Umbraco.Core.Security { Expires = DateTime.Now.AddMinutes(minutesPersisted), Domain = cookieDomain, - Path = cookiePath + Path = "/" }; if (GlobalSettings.UseSSL) diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs new file mode 100644 index 0000000000..54b537faab --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory + { + /// + /// Create a ClaimsIdentity from a user + /// + /// + /// + public override async Task CreateAsync(UserManager manager, BackOfficeIdentityUser user, string authenticationType) + { + var baseIdentity = await base.CreateAsync(manager, user, authenticationType); + + var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, new UserData() + { + Id = user.Id, + Username = user.UserName, + RealName = user.Name, + AllowedApplications = user.AllowedApplications, + Culture = user.Culture, + Roles = user.Roles.Select(x => x.RoleId).ToArray(), + StartContentNode = user.StartContentNode, + StartMediaNode = user.StartMediaNode + }); + + return umbracoIdentity; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs new file mode 100644 index 0000000000..9634b17c73 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + /// + /// A custom password hasher that conforms to the current password hashing done in Umbraco + /// + internal class MembershipPasswordHasher : IPasswordHasher + { + private readonly MembershipProviderBase _provider; + + public MembershipPasswordHasher(MembershipProviderBase provider) + { + _provider = provider; + } + + public string HashPassword(string password) + { + return _provider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return _provider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } + + /// + /// Back office user manager + /// + public class BackOfficeUserManager : UserManager + { + public BackOfficeUserManager(IUserStore store) + : base(store) + { + } + + #region What we support currently + + //TODO: Support this + public override bool SupportsUserRole + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsQueryableUsers + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserLockout + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserSecurityStamp + { + get { return false; } + } + + public override bool SupportsUserTwoFactor + { + get { return false; } + } + + public override bool SupportsUserPhoneNumber + { + get { return false; } + } + #endregion + + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + IOwinContext context, + IUserService userService, + IExternalLoginService externalLoginService, + MembershipProviderBase membershipProvider) + { + if (options == null) throw new ArgumentNullException("options"); + if (context == null) throw new ArgumentNullException("context"); + if (userService == null) throw new ArgumentNullException("userService"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); + + // Configure validation logic for usernames + manager.UserValidator = new UserValidator(manager) + { + AllowOnlyAlphanumericUserNames = false, + RequireUniqueEmail = true + }; + + // Configure validation logic for passwords + manager.PasswordValidator = new PasswordValidator + { + RequiredLength = membershipProvider.MinRequiredPasswordLength, + RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, + RequireDigit = false, + RequireLowercase = false, + RequireUppercase = false + }; + + //use a custom hasher based on our membership provider + manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); + + var dataProtectionProvider = options.DataProtectionProvider; + if (dataProtectionProvider != null) + { + manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + } + + //custom identity factory for creating the identity object for which we auth against in the back office + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + + //NOTE: Not implementing these currently + + //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user + //// You can write your own provider and plug in here. + //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider + //{ + // MessageFormat = "Your security code is: {0}" + //}); + //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider + //{ + // Subject = "Security Code", + // BodyFormat = "Your security code is: {0}" + //}); + + //manager.EmailService = new EmailService(); + //manager.SmsService = new SmsService(); + + return manager; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + } +} diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs new file mode 100644 index 0000000000..427bc306da --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore + { + private readonly IUserService _userService; + private readonly IExternalLoginService _externalLoginService; + private readonly MembershipProviderBase _usersMembershipProvider; + + public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) + { + _userService = userService; + _externalLoginService = externalLoginService; + _usersMembershipProvider = usersMembershipProvider; + if (userService == null) throw new ArgumentNullException("userService"); + if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + _userService = userService; + _usersMembershipProvider = usersMembershipProvider; + _externalLoginService = externalLoginService; + + if (_usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) + { + throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); + } + } + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + throw new NotImplementedException(); + } + + /// + /// Insert a new user + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Update a user + /// + /// + /// + public Task UpdateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Delete a user + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Finds a user + /// + /// + /// + public Task FindByIdAsync(int userId) + { + throw new NotImplementedException(); + } + + /// + /// Find a user by name + /// + /// + /// + public Task FindByNameAsync(string userName) + { + throw new NotImplementedException(); + } + + /// + /// Set the user password hash + /// + /// + /// + public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) + { + throw new NotImplementedException(); + } + + /// + /// Get the user password hash + /// + /// + /// + public Task GetPasswordHashAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if a user has a password set + /// + /// + /// + public Task HasPasswordAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Set the user email + /// + /// + /// + public Task SetEmailAsync(BackOfficeIdentityUser user, string email) + { + throw new NotImplementedException(); + } + + /// + /// Get the user email + /// + /// + /// + public Task GetEmailAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if the user email is confirmed + /// + /// + /// + public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Sets whether the user email is confirmed + /// + /// + /// + public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this email + /// + /// + /// + public Task FindByEmailAsync(string email) + { + throw new NotImplementedException(); + } + + /// + /// Adds a user login with the specified provider and key + /// + /// + /// + public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Removes the user login with the specified combination if it exists + /// + /// + /// + public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Returns the linked accounts for this user + /// + /// + /// + public Task> GetLoginsAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this login + /// + /// + public Task FindAsync(UserLoginInfo login) + { + //get all logins associated with the login id + var result = _externalLoginService.Find(login).ToArray(); + if (result.Any()) + { + //return the first member that matches the result + var user = (from l in result + select _userService.GetUserById(l.Id) + into member + where member != null + select new BackOfficeIdentityUser + { + Email = member.Email, + Id = member.Id, + LockoutEnabled = member.IsLockedOut, + LockoutEndDateUtc = DateTime.MaxValue.ToUniversalTime(), + UserName = member.Username, + PasswordHash = GetPasswordHash(member.RawPasswordValue) + }).FirstOrDefault(); + + return Task.FromResult(AssignLoginsCallback(user)); + } + + return Task.FromResult(null); + } + + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) + { + if (user != null) + { + user.SetLoginsCallback(new Lazy>(() => + _externalLoginService.GetAll(user.Id))); + } + return user; + } + + private string GetPasswordHash(string storedPass) + { + return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index ebcd967cc2..e39919d291 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -16,6 +16,19 @@ namespace Umbraco.Core.Security ///
/// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> ///<media> @@ -83,26 +73,6 @@ namespace Umbraco.Tests.UmbracoExamine { } } - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] PDFStandards { - get { - object obj = ResourceManager.GetObject("PDFStandards", resourceCulture); - return ((byte[])(obj)); - } - } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] SurviorFlipCup { - get { - object obj = ResourceManager.GetObject("SurviorFlipCup", resourceCulture); - return ((byte[])(obj)); - } - } - /// /// Looks up a localized string similar to <?xml version="1.0" encoding="utf-8"?> ///<!DOCTYPE root[ @@ -148,15 +118,5 @@ namespace Umbraco.Tests.UmbracoExamine { return ResourceManager.GetString("umbraco_sort", resourceCulture); } } - - /// - /// Looks up a localized resource of type System.Byte[]. - /// - internal static byte[] windows_vista { - get { - object obj = ResourceManager.GetObject("windows_vista", resourceCulture); - return ((byte[])(obj)); - } - } } } diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx b/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx index 5e4f836470..7dfde4fbad 100644 --- a/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx +++ b/src/Umbraco.Tests/UmbracoExamine/TestFiles.resx @@ -118,25 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - testfiles\converting_file_to_pdf.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - testfiles\media.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - testfiles\pdfstandards.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - testfiles\umbraco.config;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 testfiles\umbraco-sort.config;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 - - testfiles\windows-vista.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - testfiles\surviorflipcup.pdf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf b/src/Umbraco.Tests/UmbracoExamine/TestFiles/Converting_file_to_PDF.pdf deleted file mode 100644 index 94dce8253e70a9e649b64caf222b9de4ee284574..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118488 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

#mxhR3M$+}1|iILK7WdiP9i#*JIty|9-2 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 z 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

4@>v1w36_oLNea#8y!j4C+E~E9+-ZAP?+%g0c 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�|m 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 IB1iat0Lw>3Z~y=R diff --git a/src/umbraco.sln b/src/umbraco.sln index 6fb7afde81..7c550c17ba 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -19,11 +19,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-184C-4005-A5F3-E705D92FC645}" ProjectSection(SolutionItems) = preProject - ..\docs\License.txt = ..\docs\License.txt - ..\docs\README.txt = ..\docs\README.txt + ..\LICENSE.md = ..\LICENSE.md + ..\README.md = ..\README.md EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{B5BD12C1-A454-435E-8A46-FF4A364C0382}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1-A454-435E-8A46-FF4A364C0382}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject From 051e9366e5f2f76ea0590aeee1417238366ffa71 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 13:39:54 +1100 Subject: [PATCH 136/249] updates js undefined check --- .../propertyeditors/contentpicker/contentpicker.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 9002535488..8c656b2d4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -175,7 +175,7 @@ function contentPickerController($scope, dialogService, entityResource, editorSt return d.id == id; }); - if(entity != undefined) { + if(entity) { entity.icon = iconHelper.convertFromLegacyIcon(entity.icon); $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon }); } From 028ddfe290b6b49eeef270659d61f05ec5da59b6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 19:24:59 +1100 Subject: [PATCH 137/249] Starts adding asp.net identity --- .../src/common/services/user.service.js | 8 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 27 +++++- src/Umbraco.Web.UI/packages.config | 8 ++ .../Security/Identity/AppBuilderExtensions.cs | 91 +++++++++++++++++++ .../Security/Identity/OwinExtensions.cs | 19 ++++ .../UmbracoBackOfficeAuthenticationHandler.cs | 87 ++++++++++++++++++ ...bracoBackOfficeAuthenticationMiddleware.cs | 26 ++++++ .../UmbracoBackOfficeAuthenticationOptions.cs | 18 ++++ src/Umbraco.Web/Umbraco.Web.csproj | 26 ++++++ src/Umbraco.Web/packages.config | 7 ++ 10 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs create mode 100644 src/Umbraco.Web/Security/Identity/OwinExtensions.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 85578dbb99..8d99086ba5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -56,7 +56,7 @@ angular.module('umbraco.services') /** Method to count down the current user's timeout seconds, - this will continually count down their current remaining seconds every 2 seconds until + this will continually count down their current remaining seconds every 5 seconds until there are no more seconds remaining. */ function countdownUserTimeout() { @@ -64,8 +64,8 @@ angular.module('umbraco.services') $timeout(function () { if (currentUser) { - //countdown by 2 seconds since that is how long our timer is for. - currentUser.remainingAuthSeconds -= 2; + //countdown by 5 seconds since that is how long our timer is for. + currentUser.remainingAuthSeconds -= 5; //if there are more than 30 remaining seconds, recurse! if (currentUser.remainingAuthSeconds > 30) { @@ -128,7 +128,7 @@ angular.module('umbraco.services') } } } - }, 2000, //every 2 seconds + }, 5000, //every 5 seconds false); //false = do NOT execute a digest for every iteration } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f51d61b15c..c2de5441ab 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -151,7 +151,29 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + False + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -178,6 +200,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + System @@ -324,6 +349,7 @@ Properties\SolutionInfo.cs + loadStarterKits.ascx ASPXCodeBehind @@ -2518,7 +2544,6 @@ - diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index da11256606..150759f582 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -9,6 +9,8 @@ + + @@ -21,10 +23,16 @@ + + + + + + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs new file mode 100644 index 0000000000..f2ef0a010d --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Security.Identity +{ + public static class AppBuilderExtensions + { + /////

+ ///// Configure Identity User Manager for Umbraco + ///// + ///// + ///// + ///// + //public static void ConfigureUserManagerForUmbraco(this IAppBuilder app, ApplicationContext appContext) + // where T : UmbracoIdentityUser, new() + //{ + + // //Don't proceed if the app is not ready + // if (appContext.IsConfigured == false + // || appContext.DatabaseContext == null + // || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + // //Configure Umbraco user manager to be created per request + // app.CreatePerOwinContext>( + // (o, c) => UmbracoMembersUserManager.Create( + // o, c, ApplicationContext.Current.Services.MemberService)); + + // //Configure Umbraco member event handler to be created per request - this will ensure that the + // // external logins are kept in sync if members are deleted from Umbraco + // app.CreatePerOwinContext>((options, context) => new MembersEventHandler(context)); + + // //TODO: This is just for the mem leak fix + // app.CreatePerOwinContext, UmbracoMembersUserManager>>( + // (o, c) => new OwinContextDisposal, UmbracoMembersUserManager>(c)); + //} + + /// + /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline + /// + /// + /// + public static IAppBuilder UseUmbracoBackAuthentication(this IAppBuilder app) + { + if (app == null) throw new ArgumentNullException("app"); + + app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), + //ctor params + app, new UmbracoBackOfficeAuthenticationOptions(), UmbracoConfig.For.UmbracoSettings().Security); + + app.UseStageMarker(PipelineStage.Authenticate); + return app; + } + + //This is a fix for OWIN mem leak! + //http://stackoverflow.com/questions/24378856/memory-leak-in-owin-appbuilderextensions/24819543#24819543 + private class OwinContextDisposal : IDisposable + where T1 : IDisposable + where T2 : IDisposable + { + private readonly List _disposables = new List(); + private bool _disposed = false; + + public OwinContextDisposal(IOwinContext owinContext) + { + if (HttpContext.Current == null) return; + + _disposables.Add(owinContext.Get()); + _disposables.Add(owinContext.Get()); + + HttpContext.Current.DisposeOnPipelineCompleted(this); + } + + public void Dispose() + { + if (_disposed) return; + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + _disposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs new file mode 100644 index 0000000000..4b83f97bd3 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs @@ -0,0 +1,19 @@ +using System.Web; +using Microsoft.Owin; + +namespace Umbraco.Web.Security.Identity +{ + internal static class OwinExtensions + { + /// + /// Nasty little hack to get httpcontextbase from an owin context + /// + /// + /// + public static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + { + return owinContext.Get(typeof(HttpContextBase).FullName); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs new file mode 100644 index 0000000000..1b0adc604c --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs @@ -0,0 +1,87 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Security; +using Umbraco.Core; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Used to allow normal Umbraco back office authentication to work + /// + public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler + { + private readonly ISecuritySection _securitySection; + + public UmbracoBackOfficeAuthenticationHandler(ISecuritySection securitySection) + { + _securitySection = securitySection; + } + + /// + /// Checks if we should authentication the request (i.e. is back office) and if so gets the forms auth ticket in the request + /// and returns an AuthenticationTicket based on that. + /// + /// + /// + /// It's worth noting that the UmbracoModule still executes and performs the authentication, however this also needs to execute + /// so that it assigns the new Principal object on the OWIN request: + /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ + /// + protected override Task AuthenticateCoreAsync() + { + if (ShouldAuthRequest()) + { + var authTicket = GetAuthTicket(Request, _securitySection.AuthCookieName); + if (authTicket != null) + { + return Task.FromResult(new AuthenticationTicket(new UmbracoBackOfficeIdentity(authTicket), new AuthenticationProperties())); + } + } + + return Task.FromResult(null); + } + + private bool ShouldAuthRequest() + { + var httpContext = Context.HttpContextFromOwinContext(); + + // do not process if client-side request + if (httpContext.Request.Url.IsClientSideRequest()) + return false; + + return UmbracoModule.ShouldAuthenticateRequest(httpContext.Request, Request.Uri); + } + + /// + /// Returns the current FormsAuth ticket in the request + /// + /// + /// + /// + private static FormsAuthenticationTicket GetAuthTicket(IOwinRequest request, string cookieName) + { + if (request == null) throw new ArgumentNullException("request"); + + var formsCookie = request.Cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + //get the ticket + try + { + return FormsAuthentication.Decrypt(formsCookie); + } + catch (Exception) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs new file mode 100644 index 0000000000..1275b72c08 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs @@ -0,0 +1,26 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security.Infrastructure; +using Owin; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Used to enable the normal Umbraco back office authentication to operate + /// + public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ISecuritySection _securitySection; + + public UmbracoBackOfficeAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, UmbracoBackOfficeAuthenticationOptions options, ISecuritySection securitySection) + : base(next, options) + { + _securitySection = securitySection; + } + + protected override AuthenticationHandler CreateHandler() + { + return new UmbracoBackOfficeAuthenticationHandler(_securitySection); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs new file mode 100644 index 0000000000..c7609a8e83 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs @@ -0,0 +1,18 @@ +using Microsoft.Owin.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Umbraco auth options - really just ensures that it is operating in Active mode + /// + public sealed class UmbracoBackOfficeAuthenticationOptions : AuthenticationOptions + { + public UmbracoBackOfficeAuthenticationOptions() + : base("UmbracoBackOffice") + { + //Must be active, this needs to look at each request to determine if it should execute, + // if set to passive this will not be the case + AuthenticationMode = AuthenticationMode.Active; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e261c048d3..45e0ce5fe2 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -131,7 +131,25 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + ..\packages\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -148,6 +166,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + System @@ -518,6 +539,11 @@ + + + + + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 294079e830..3fade0702d 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,6 +6,8 @@ + + @@ -17,9 +19,14 @@ + + + + + From 93df2edec2c247ef6615496ed75195580f7b71da Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 13:47:00 +1100 Subject: [PATCH 138/249] Initial install which now uses Identity middleware to perform the back office auth (no longer done in our module). Created custom data secure classes that use the legacy Forms auth logic for backwards compat. This means that the cookie can still be written the old way and still auth the new way if required. Now need to clean a lot of this up. --- .../Security/AuthenticationExtensions.cs | 49 ++++- src/Umbraco.Core/Umbraco.Core.csproj | 6 + src/Umbraco.Core/packages.config | 2 + src/Umbraco.Tests/App.config | 8 + src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 61 ++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../Editors/AuthenticationController.cs | 11 ++ .../Security/Identity/AppBuilderExtensions.cs | 8 +- .../FormsAuthenticationSecureDataFormat.cs | 62 ++++++ .../UmbracoBackOfficeAuthenticationHandler.cs | 179 ++++++++++++++++-- ...bracoBackOfficeAuthenticationMiddleware.cs | 17 +- .../UmbracoBackOfficeAuthenticationOptions.cs | 18 -- ...coBackOfficeCookieAuthenticationOptions.cs | 35 ++++ src/Umbraco.Web/Security/WebSecurity.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 8 +- src/Umbraco.Web/UmbracoModule.cs | 30 +-- src/Umbraco.Web/app.config | 4 + src/Umbraco.Web/packages.config | 2 +- src/umbraco.MacroEngines/app.config | 4 + src/umbraco.businesslogic/packages.config | 2 + .../umbraco.businesslogic.csproj | 6 + src/umbraco.editorControls/app.config | 4 + 22 files changed, 454 insertions(+), 66 deletions(-) create mode 100644 src/Umbraco.Web.UI/App_Code/OwinStartup.cs create mode 100644 src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 8511d39125..49511697d7 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,10 +1,15 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Principal; using System.Threading; using System.Web; using System.Web.Security; +using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; @@ -268,6 +273,23 @@ namespace Umbraco.Core.Security return new HttpContextWrapper(http).GetUmbracoAuthTicket(); } + internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this IOwinContext ctx) + { + if (ctx == null) throw new ArgumentNullException("ctx"); + //get the ticket + try + { + return GetAuthTicket(ctx.Request.Cookies.ToDictionary(x => x.Key, x => x.Value), UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); + } + catch (Exception) + { + //TODO: Do we need to do more here?? need to make sure that the forms cookie is gone, but is that + // taken care of in our custom middleware somehow? + ctx.Authentication.SignOut(); + return null; + } + } + /// /// This clears the forms authentication cookie /// @@ -301,16 +323,18 @@ namespace Umbraco.Core.Security private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { - if (http == null) throw new ArgumentNullException("http"); - var formsCookie = http.Request.Cookies[cookieName]; - if (formsCookie == null) + var allKeys = new List(); + for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { - return null; + allKeys.Add(http.Request.Cookies.Keys.Get(i)); } + var asDictionary = allKeys.ToDictionary(key => key, key => http.Request.Cookies[key].Value); + //get the ticket try { - return FormsAuthentication.Decrypt(formsCookie.Value); + + return GetAuthTicket(asDictionary, cookieName); } catch (Exception) { @@ -320,6 +344,21 @@ namespace Umbraco.Core.Security } } + private static FormsAuthenticationTicket GetAuthTicket(IDictionary cookies, string cookieName) + { + if (cookies == null) throw new ArgumentNullException("cookies"); + + if (cookies.ContainsKey(cookieName) == false) return null; + + var formsCookie = cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + //get the ticket + return FormsAuthentication.Decrypt(formsCookie); + } + /// /// Renews the forms authentication ticket & cookie /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7cdb815933..9579bbba59 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,6 +54,9 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -70,6 +73,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index f3f9b617f7..c13e24ef1a 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -10,10 +10,12 @@ + + \ No newline at end of file diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 3d35ae5c93..86e6a0cbfe 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -152,6 +152,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs new file mode 100644 index 0000000000..e215fd552c --- /dev/null +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; + +using Owin; +using Umbraco.Web.Security.Identity; +using Umbraco.Web.UI; + +[assembly: OwinStartup(typeof(OwinStartup))] + +namespace Umbraco.Web.UI +{ + + /// + /// Summary description for Startup + /// + public class OwinStartup + { + + public void Configuration(IAppBuilder app) + { + ////Single method to configure the Identity user manager for use with Umbraco + //app.ConfigureUserManagerForUmbraco(); + + //// Enable the application to use a cookie to store information for the + //// signed in user and to use a cookie to temporarily store information + //// about a user logging in with a third party login provider + //// Configure the sign in cookie + //app.UseCookieAuthentication(new CookieAuthenticationOptions + //{ + // AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, + + // Provider = new CookieAuthenticationProvider + // { + // // Enables the application to validate the security stamp when the user + // // logs in. This is a security feature which is used when you + // // change a password or add an external login to your account. + // OnValidateIdentity = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + // } + //}); + + //Ensure owin is configured for Umbraco back office authentication - this must + // be configured AFTER the standard UseCookieConfiguration above. + app.UseUmbracoBackAuthentication(); + + app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c2de5441ab..fb07ae1ff4 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2579,7 +2579,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7300 + http://localhost:7301 False False diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 00498f7e61..e3f13d46f3 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -9,6 +9,9 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Security; using AutoMapper; +using Microsoft.AspNet.Identity; +using Microsoft.Owin; +using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; @@ -109,6 +112,14 @@ namespace Umbraco.Web.Editors //TODO: Clean up the int cast! var ticket = UmbracoContext.Security.PerformLogin(user); + //TODO: Normally we'd do something like this for identity, but we're mixing and matching legacy and new here + // so we'll keep the legacy way and move forward with this in our custom handler for now, eventually replacing + // the above legacy logic with the new stuff. + + //OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); + //OwinContext.Authentication.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, + // await user.GenerateUserIdentityAsync(UserManager)); + var http = this.TryGetHttpContext(); if (http.Success == false) { diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index f2ef0a010d..dc68a43152 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Owin.Extensions; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { @@ -52,7 +53,12 @@ namespace Umbraco.Web.Security.Identity app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), //ctor params - app, new UmbracoBackOfficeAuthenticationOptions(), UmbracoConfig.For.UmbracoSettings().Security); + app, + new UmbracoBackOfficeCookieAuthenticationOptions( + UmbracoConfig.For.UmbracoSettings().Security, + GlobalSettings.TimeOutInMinutes, + GlobalSettings.UseSSL), + LoggerResolver.Current.Logger); app.UseStageMarker(PipelineStage.Authenticate); return app; diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs new file mode 100644 index 0000000000..fac130f0ca --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs @@ -0,0 +1,62 @@ +using System; +using System.Web.Security; +using Microsoft.Owin.Security; +using Newtonsoft.Json; +using Umbraco.Core.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Custom secure format that uses the old FormsAuthentication format + /// + internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat + { + private readonly int _loginTimeoutMinutes; + + public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes) + { + _loginTimeoutMinutes = loginTimeoutMinutes; + } + + public string Protect(AuthenticationTicket data) + { + //TODO: Where to get the user data? + //var userDataString = JsonConvert.SerializeObject(userdata); + + var ticket = new FormsAuthenticationTicket( + 5, + data.Identity.Name, + data.Properties.IssuedUtc.HasValue ? data.Properties.IssuedUtc.Value.LocalDateTime : DateTime.Now, + data.Properties.ExpiresUtc.HasValue ? data.Properties.ExpiresUtc.Value.LocalDateTime : DateTime.Now.AddMinutes(_loginTimeoutMinutes), + data.Properties.IsPersistent, + "", //User data here!! This will come from the identity + "/" + ); + + return FormsAuthentication.Encrypt(ticket); + } + + public AuthenticationTicket Unprotect(string protectedText) + { + FormsAuthenticationTicket decrypt; + try + { + decrypt = FormsAuthentication.Decrypt(protectedText); + if (decrypt == null) return null; + } + catch (Exception) + { + return null; + } + + var identity = new UmbracoBackOfficeIdentity(decrypt); + + return new AuthenticationTicket(identity, new AuthenticationProperties + { + ExpiresUtc = decrypt.Expiration.ToUniversalTime(), + IssuedUtc = decrypt.IssueDate.ToUniversalTime(), + IsPersistent = decrypt.IsPersistent + }); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs index 1b0adc604c..3393075c14 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs @@ -1,26 +1,35 @@ using System; using System.Reflection; +using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; +using System.Web; using System.Web.Security; using Microsoft.Owin; using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Infrastructure; +using Newtonsoft.Json; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Security; using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { /// /// Used to allow normal Umbraco back office authentication to work /// - public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler + public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler { - private readonly ISecuritySection _securitySection; + private readonly ILogger _logger; + private bool _shouldRenew; + private DateTimeOffset _renewIssuedUtc; + private DateTimeOffset _renewExpiresUtc; - public UmbracoBackOfficeAuthenticationHandler(ISecuritySection securitySection) + public UmbracoBackOfficeAuthenticationHandler(ILogger logger) { - _securitySection = securitySection; + _logger = logger; } /// @@ -33,24 +42,165 @@ namespace Umbraco.Web.Security.Identity /// so that it assigns the new Principal object on the OWIN request: /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ /// - protected override Task AuthenticateCoreAsync() + protected override async Task AuthenticateCoreAsync() { if (ShouldAuthRequest()) { - var authTicket = GetAuthTicket(Request, _securitySection.AuthCookieName); - if (authTicket != null) + var ticket = GetAuthTicket(Request); + + if (ticket == null) { - return Task.FromResult(new AuthenticationTicket(new UmbracoBackOfficeIdentity(authTicket), new AuthenticationProperties())); + _logger.Warn(@"Unprotect ticket failed"); + return null; } + + DateTimeOffset currentUtc = Options.SystemClock.UtcNow; + DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; + DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc != null && expiresUtc.Value < currentUtc) + { + return null; + } + + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) + { + TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); + TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + _shouldRenew = true; + _renewIssuedUtc = currentUtc; + TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _renewExpiresUtc = currentUtc.Add(timeSpan); + } + } + + var context = new CookieValidateIdentityContext(Context, ticket, Options); + + await Options.Provider.ValidateIdentity(context); + + return new AuthenticationTicket(context.Identity, context.Properties); } - return Task.FromResult(null); + return await Task.FromResult(null); + } + + protected override async Task ApplyResponseGrantAsync() + { + AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType); + bool shouldSignin = signin != null; + AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); + bool shouldSignout = signout != null; + + if (shouldSignin || shouldSignout || _shouldRenew) + { + var cookieOptions = new CookieOptions + { + Domain = Options.CookieDomain, + HttpOnly = Options.CookieHttpOnly, + Path = Options.CookiePath ?? "/", + }; + if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = Request.IsSecure; + } + else + { + cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + } + + if (shouldSignin) + { + var context = new CookieResponseSignInContext( + Context, + Options, + Options.AuthenticationType, + signin.Identity, + signin.Properties); + + DateTimeOffset issuedUtc = Options.SystemClock.UtcNow; + DateTimeOffset expiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); + + context.Properties.IssuedUtc = issuedUtc; + context.Properties.ExpiresUtc = expiresUtc; + + Options.Provider.ResponseSignIn(context); + + if (context.Properties.IsPersistent) + { + cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + } + + var model = new AuthenticationTicket(context.Identity, context.Properties); + string cookieValue = Options.TicketDataFormat.Protect(model); + + Response.Cookies.Append( + Options.CookieName, + cookieValue, + cookieOptions); + } + else if (shouldSignout) + { + Response.Cookies.Delete( + Options.CookieName, + cookieOptions); + } + else if (_shouldRenew) + { + AuthenticationTicket model = await AuthenticateAsync(); + + model.Properties.IssuedUtc = _renewIssuedUtc; + model.Properties.ExpiresUtc = _renewExpiresUtc; + + string cookieValue = Options.TicketDataFormat.Protect(model); + + if (model.Properties.IsPersistent) + { + cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; + } + + Response.Cookies.Append( + Options.CookieName, + cookieValue, + cookieOptions); + } + + //Response.Headers.Set( + // HeaderNameCacheControl, + // HeaderValueNoCache); + + //Response.Headers.Set( + // HeaderNamePragma, + // HeaderValueNoCache); + + //Response.Headers.Set( + // HeaderNameExpires, + // HeaderValueMinusOne); + + bool shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; + bool shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; + + if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) + { + IReadableStringCollection query = Request.Query; + string redirectUri = query.Get(Options.ReturnUrlParameter); + if (!string.IsNullOrWhiteSpace(redirectUri) + //&& IsHostRelative(redirectUri) + ) + { + var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); + Options.Provider.ApplyRedirect(redirectContext); + } + } + } } private bool ShouldAuthRequest() { var httpContext = Context.HttpContextFromOwinContext(); - + // do not process if client-side request if (httpContext.Request.Url.IsClientSideRequest()) return false; @@ -62,21 +212,20 @@ namespace Umbraco.Web.Security.Identity /// Returns the current FormsAuth ticket in the request /// /// - /// /// - private static FormsAuthenticationTicket GetAuthTicket(IOwinRequest request, string cookieName) + private AuthenticationTicket GetAuthTicket(IOwinRequest request) { if (request == null) throw new ArgumentNullException("request"); - var formsCookie = request.Cookies[cookieName]; - if (formsCookie == null) + var formsCookie = request.Cookies[Options.CookieName]; + if (string.IsNullOrWhiteSpace(formsCookie)) { return null; } //get the ticket try { - return FormsAuthentication.Decrypt(formsCookie); + return Options.TicketDataFormat.Unprotect(formsCookie); } catch (Exception) { diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs index 1275b72c08..ffdce0fc8d 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs @@ -2,25 +2,30 @@ using Microsoft.Owin.Security.Infrastructure; using Owin; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { /// /// Used to enable the normal Umbraco back office authentication to operate /// - public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware + public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware { - private readonly ISecuritySection _securitySection; + private readonly ILogger _logger; - public UmbracoBackOfficeAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, UmbracoBackOfficeAuthenticationOptions options, ISecuritySection securitySection) + public UmbracoBackOfficeAuthenticationMiddleware( + OwinMiddleware next, + IAppBuilder app, + UmbracoBackOfficeCookieAuthenticationOptions options, + ILogger logger) : base(next, options) { - _securitySection = securitySection; + _logger = logger; } - protected override AuthenticationHandler CreateHandler() + protected override AuthenticationHandler CreateHandler() { - return new UmbracoBackOfficeAuthenticationHandler(_securitySection); + return new UmbracoBackOfficeAuthenticationHandler(_logger); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs deleted file mode 100644 index c7609a8e83..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Owin.Security; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Umbraco auth options - really just ensures that it is operating in Active mode - /// - public sealed class UmbracoBackOfficeAuthenticationOptions : AuthenticationOptions - { - public UmbracoBackOfficeAuthenticationOptions() - : base("UmbracoBackOffice") - { - //Must be active, this needs to look at each request to determine if it should execute, - // if set to passive this will not be the case - AuthenticationMode = AuthenticationMode.Active; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs new file mode 100644 index 0000000000..e23f30b27f --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -0,0 +1,35 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Umbraco auth cookie options + /// + public sealed class UmbracoBackOfficeCookieAuthenticationOptions : CookieAuthenticationOptions + { + public UmbracoBackOfficeCookieAuthenticationOptions() + : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) + { + } + + public UmbracoBackOfficeCookieAuthenticationOptions(ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl) + { + AuthenticationType = "UmbracoBackOffice"; + + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + + CookieDomain = securitySection.AuthCookieDomain; + CookieName = securitySection.AuthCookieName; + CookieHttpOnly = true; + CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; + CookiePath = "/"; + LoginPath = new PathString("/umbraco/login"); //TODO: ?? + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 0cad922d16..b6ec3680d8 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -91,7 +91,7 @@ namespace Umbraco.Web.Security /// Logs the user in ///

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 diff --git a/src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF b/src/Umbraco.Tests/UmbracoExamine/TestFiles/PDFStandards.PDF deleted file mode 100644 index 060beac5ef10b2bfb92749b98a2f36b81ebc0532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54932 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

/// - /// returns the number of seconds until their session times out + /// returns the Forms Auth ticket created which is used to log them in public virtual FormsAuthenticationTicket PerformLogin(IUser user) { var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N")) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 45e0ce5fe2..858561e5e9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,8 +138,9 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll + + False + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll @@ -540,10 +541,11 @@ + - + diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index f2d216f2fd..719f7135c4 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -180,26 +180,26 @@ namespace Umbraco.Web /// static void AuthenticateRequest(object sender, EventArgs e) { - var app = (HttpApplication)sender; - var http = new HttpContextWrapper(app.Context); + //var app = (HttpApplication)sender; + //var http = new HttpContextWrapper(app.Context); - // do not process if client-side request - if (http.Request.Url.IsClientSideRequest()) - return; + //// do not process if client-side request + //if (http.Request.Url.IsClientSideRequest()) + // return; - var req = new HttpRequestWrapper(app.Request); + //var req = new HttpRequestWrapper(app.Request); - if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) - { - //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works - // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler - // executes. If it is successful, it doesn't call next and assigns the current user/principal. - // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. + //if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) + //{ + // //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works + // // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler + // // executes. If it is successful, it doesn't call next and assigns the current user/principal. + // // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. - var ticket = http.GetUmbracoAuthTicket(); + // var ticket = http.GetUmbracoAuthTicket(); - http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); - } + // http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); + //} } diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index 957569042f..051211ebf7 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -51,6 +51,10 @@ + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 3fade0702d..8506c1de5b 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -19,7 +19,7 @@ - + diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index 900c3903d5..a3f8757270 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -26,6 +26,10 @@ + + + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/packages.config b/src/umbraco.businesslogic/packages.config index 59350953ba..8ae1655c0d 100644 --- a/src/umbraco.businesslogic/packages.config +++ b/src/umbraco.businesslogic/packages.config @@ -4,5 +4,7 @@ + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index c4c675366f..7accd2ccb5 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -113,10 +113,16 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 734aeed7b8..743b7c93ca 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -31,6 +31,10 @@ + + + + \ No newline at end of file From 48317d7e619778625d08a87e8566dfa49516a286 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 14:05:29 +1100 Subject: [PATCH 139/249] massively simplifies the cookie handling, we don't use our own and just use the defaults, the trick to not validating everything is to use the cookie path. This does mean that each clientside request will also be validated but there's no way to override this behavior in identity currently, the cookie handler is internal so unless we copy/paste all of it's code can't do much about that. --- .../Security/AuthenticationExtensions.cs | 5 +- .../Security/Identity/AppBuilderExtensions.cs | 55 ++-- .../UmbracoBackOfficeAuthenticationHandler.cs | 236 ------------------ ...bracoBackOfficeAuthenticationMiddleware.cs | 31 --- ...coBackOfficeCookieAuthenticationOptions.cs | 21 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 - 6 files changed, 38 insertions(+), 312 deletions(-) delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 49511697d7..008f24f492 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -216,7 +216,7 @@ namespace Umbraco.Core.Security GlobalSettings.TimeOutInMinutes, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440, - "/", + "/umbraco", UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain); } @@ -443,7 +443,8 @@ namespace Umbraco.Core.Security hash) { Expires = DateTime.Now.AddMinutes(minutesPersisted), - Domain = cookieDomain + Domain = cookieDomain, + Path = cookiePath }; if (GlobalSettings.UseSSL) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index dc68a43152..52eadd98da 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Web; +using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; +using Microsoft.Owin.Security.Cookies; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -51,47 +53,28 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), - //ctor params - app, - new UmbracoBackOfficeCookieAuthenticationOptions( + + app.UseCookieAuthentication(new UmbracoBackOfficeCookieAuthenticationOptions( UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL), - LoggerResolver.Current.Logger); + GlobalSettings.UseSSL, + GlobalSettings.Path) + { + //Provider = new CookieAuthenticationProvider + //{ + // // Enables the application to validate the security stamp when the user + // // logs in. This is a security feature which is used when you + // // change a password or add an external login to your account. + // OnValidateIdentity = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + //} + }); - app.UseStageMarker(PipelineStage.Authenticate); return app; } - //This is a fix for OWIN mem leak! - //http://stackoverflow.com/questions/24378856/memory-leak-in-owin-appbuilderextensions/24819543#24819543 - private class OwinContextDisposal : IDisposable - where T1 : IDisposable - where T2 : IDisposable - { - private readonly List _disposables = new List(); - private bool _disposed = false; - - public OwinContextDisposal(IOwinContext owinContext) - { - if (HttpContext.Current == null) return; - - _disposables.Add(owinContext.Get()); - _disposables.Add(owinContext.Get()); - - HttpContext.Current.DisposeOnPipelineCompleted(this); - } - - public void Dispose() - { - if (_disposed) return; - foreach (var disposable in _disposables) - { - disposable.Dispose(); - } - _disposed = true; - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs deleted file mode 100644 index 3393075c14..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Reflection; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Security; -using Microsoft.Owin; -using Microsoft.Owin.Security; -using Microsoft.Owin.Security.Cookies; -using Microsoft.Owin.Security.Infrastructure; -using Newtonsoft.Json; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Security; -using Umbraco.Core; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Used to allow normal Umbraco back office authentication to work - /// - public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private bool _shouldRenew; - private DateTimeOffset _renewIssuedUtc; - private DateTimeOffset _renewExpiresUtc; - - public UmbracoBackOfficeAuthenticationHandler(ILogger logger) - { - _logger = logger; - } - - /// - /// Checks if we should authentication the request (i.e. is back office) and if so gets the forms auth ticket in the request - /// and returns an AuthenticationTicket based on that. - /// - /// - /// - /// It's worth noting that the UmbracoModule still executes and performs the authentication, however this also needs to execute - /// so that it assigns the new Principal object on the OWIN request: - /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ - /// - protected override async Task AuthenticateCoreAsync() - { - if (ShouldAuthRequest()) - { - var ticket = GetAuthTicket(Request); - - if (ticket == null) - { - _logger.Warn(@"Unprotect ticket failed"); - return null; - } - - DateTimeOffset currentUtc = Options.SystemClock.UtcNow; - DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; - DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; - - if (expiresUtc != null && expiresUtc.Value < currentUtc) - { - return null; - } - - if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) - { - TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); - TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); - - if (timeRemaining < timeElapsed) - { - _shouldRenew = true; - _renewIssuedUtc = currentUtc; - TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); - _renewExpiresUtc = currentUtc.Add(timeSpan); - } - } - - var context = new CookieValidateIdentityContext(Context, ticket, Options); - - await Options.Provider.ValidateIdentity(context); - - return new AuthenticationTicket(context.Identity, context.Properties); - } - - return await Task.FromResult(null); - } - - protected override async Task ApplyResponseGrantAsync() - { - AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType); - bool shouldSignin = signin != null; - AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); - bool shouldSignout = signout != null; - - if (shouldSignin || shouldSignout || _shouldRenew) - { - var cookieOptions = new CookieOptions - { - Domain = Options.CookieDomain, - HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? "/", - }; - if (Options.CookieSecure == CookieSecureOption.SameAsRequest) - { - cookieOptions.Secure = Request.IsSecure; - } - else - { - cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; - } - - if (shouldSignin) - { - var context = new CookieResponseSignInContext( - Context, - Options, - Options.AuthenticationType, - signin.Identity, - signin.Properties); - - DateTimeOffset issuedUtc = Options.SystemClock.UtcNow; - DateTimeOffset expiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); - - context.Properties.IssuedUtc = issuedUtc; - context.Properties.ExpiresUtc = expiresUtc; - - Options.Provider.ResponseSignIn(context); - - if (context.Properties.IsPersistent) - { - cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; - } - - var model = new AuthenticationTicket(context.Identity, context.Properties); - string cookieValue = Options.TicketDataFormat.Protect(model); - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - else if (shouldSignout) - { - Response.Cookies.Delete( - Options.CookieName, - cookieOptions); - } - else if (_shouldRenew) - { - AuthenticationTicket model = await AuthenticateAsync(); - - model.Properties.IssuedUtc = _renewIssuedUtc; - model.Properties.ExpiresUtc = _renewExpiresUtc; - - string cookieValue = Options.TicketDataFormat.Protect(model); - - if (model.Properties.IsPersistent) - { - cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; - } - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - - //Response.Headers.Set( - // HeaderNameCacheControl, - // HeaderValueNoCache); - - //Response.Headers.Set( - // HeaderNamePragma, - // HeaderValueNoCache); - - //Response.Headers.Set( - // HeaderNameExpires, - // HeaderValueMinusOne); - - bool shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - bool shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; - - if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) - { - IReadableStringCollection query = Request.Query; - string redirectUri = query.Get(Options.ReturnUrlParameter); - if (!string.IsNullOrWhiteSpace(redirectUri) - //&& IsHostRelative(redirectUri) - ) - { - var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); - Options.Provider.ApplyRedirect(redirectContext); - } - } - } - } - - private bool ShouldAuthRequest() - { - var httpContext = Context.HttpContextFromOwinContext(); - - // do not process if client-side request - if (httpContext.Request.Url.IsClientSideRequest()) - return false; - - return UmbracoModule.ShouldAuthenticateRequest(httpContext.Request, Request.Uri); - } - - /// - /// Returns the current FormsAuth ticket in the request - /// - /// - /// - private AuthenticationTicket GetAuthTicket(IOwinRequest request) - { - if (request == null) throw new ArgumentNullException("request"); - - var formsCookie = request.Cookies[Options.CookieName]; - if (string.IsNullOrWhiteSpace(formsCookie)) - { - return null; - } - //get the ticket - try - { - return Options.TicketDataFormat.Unprotect(formsCookie); - } - catch (Exception) - { - return null; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs deleted file mode 100644 index ffdce0fc8d..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Owin; -using Microsoft.Owin.Security.Infrastructure; -using Owin; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Used to enable the normal Umbraco back office authentication to operate - /// - public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware - { - private readonly ILogger _logger; - - public UmbracoBackOfficeAuthenticationMiddleware( - OwinMiddleware next, - IAppBuilder app, - UmbracoBackOfficeCookieAuthenticationOptions options, - ILogger logger) - : base(next, options) - { - _logger = logger; - } - - protected override AuthenticationHandler CreateHandler() - { - return new UmbracoBackOfficeAuthenticationHandler(_logger); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs index e23f30b27f..ac77a49db3 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -13,22 +14,32 @@ namespace Umbraco.Web.Security.Identity public sealed class UmbracoBackOfficeCookieAuthenticationOptions : CookieAuthenticationOptions { public UmbracoBackOfficeCookieAuthenticationOptions() - : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) + : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL, GlobalSettings.Path) { } - public UmbracoBackOfficeCookieAuthenticationOptions(ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl) + public UmbracoBackOfficeCookieAuthenticationOptions( + ISecuritySection securitySection, + int loginTimeoutMinutes, + bool forceSsl, + string umbracoPath, + bool useLegacyFormsAuthDataFormat = true) { AuthenticationType = "UmbracoBackOffice"; - TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + if (useLegacyFormsAuthDataFormat) + { + //If this is not explicitly set it will fall back to the default automatically + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + } CookieDomain = securitySection.AuthCookieDomain; CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; - LoginPath = new PathString("/umbraco/login"); //TODO: ?? + + //Ensure the cookie path is set so that it isn't transmitted for anything apart from requests to the back office + CookiePath = umbracoPath.EnsureStartsWith('/'); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 858561e5e9..5be8656b13 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -543,8 +543,6 @@ - - From 927add6f4446afaba9d86397c1054d80175d23a2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 16:13:02 +1100 Subject: [PATCH 140/249] Updates UmbracoBackOfficeIdentity to add claims and adds a new ctor so people can create an identity manually - this is really the key, by doing this we'd already be able to have 3rd party authentication happening. Ensures our custom secure data format persists the user data --- src/Umbraco.Core/Constants-Web.cs | 12 ++ .../Security/UmbracoBackOfficeIdentity.cs | 138 ++++++++++-------- src/Umbraco.Core/Umbraco.Core.csproj | 22 +++ src/Umbraco.Core/packages.config | 5 + src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 6 +- .../Security/Identity/AppBuilderExtensions.cs | 35 +++-- .../FormsAuthenticationSecureDataFormat.cs | 17 ++- ...coBackOfficeCookieAuthenticationOptions.cs | 6 +- src/umbraco.datalayer/app.config | 4 + src/umbraco.providers/app.config | 4 + 10 files changed, 168 insertions(+), 81 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 83cb995eeb..f6c2df14c3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -18,5 +18,17 @@ public const string AuthCookieName = "UMB_UCONTEXT"; } + + public static class Security + { + + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; + public const string UserIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/userid"; + public const string CultureClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/culture"; + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 2737c636a3..be6811ad7b 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -1,115 +1,137 @@ using System; using System.Runtime.Serialization; +using System.Security.Claims; +using System.Security.Principal; using System.Web; using System.Web.Security; using Newtonsoft.Json; namespace Umbraco.Core.Security { + /// /// A custom user identity for the Umbraco backoffice /// /// - /// All values are lazy loaded for performance reasons as the constructor is called for every single request + /// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can + /// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity. /// [Serializable] public class UmbracoBackOfficeIdentity : FormsIdentity { - public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) + /// + /// Create a back office identity based on user data + /// + /// + public UmbracoBackOfficeIdentity(UserData userdata) + //This just creates a temp/fake ticket + : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) + { + UserData = userdata; + AddClaims(); + } + + /// + /// Create a new identity from a forms auth ticket + /// + /// + public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) : base(ticket) { - UserData = ticket.UserData; - EnsureDeserialized(); + UserData = JsonConvert.DeserializeObject(ticket.UserData); + AddClaims(); + } + + /// + /// Used for cloning + /// + /// + private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity) + : base(identity) + { + UserData = identity.UserData; + AddClaims(); + } + + public static string Issuer = "UmbracoBackOffice"; + + //TODO: Another option is to create a ClaimsIdentityFactory when everything is wired up... optional though i think + private void AddClaims() + { + AddClaims(new[] + { + new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.AllowedApplicationsClaimType, string.Join(",", AllowedApplications), null, Issuer, Issuer, this), + new Claim(Constants.Security.UserIdClaimType, Id.ToString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this), + new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this), + new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) + }); + } + + protected internal UserData UserData { get; private set; } + + /// + /// Gets the type of authenticated identity. + /// + /// + /// The type of authenticated identity. This property always returns "UmbracoBackOffice". + /// + public override string AuthenticationType + { + get { return Issuer; } } - - protected readonly string UserData; - internal UserData DeserializedData; public int StartContentNode { - get - { - return DeserializedData.StartContentNode; - } + get { return UserData.StartContentNode; } } public int StartMediaNode { - get { return DeserializedData.StartMediaNode; } + get { return UserData.StartMediaNode; } } public string[] AllowedApplications { - get { return DeserializedData.AllowedApplications; } + get { return UserData.AllowedApplications; } } - + public object Id { - get { return DeserializedData.Id; } + get { return UserData.Id; } } public string RealName { - get { return DeserializedData.RealName; } + get { return UserData.RealName; } } public string Culture { - get { return DeserializedData.Culture; } + get { return UserData.Culture; } } public string SessionId { - get { return DeserializedData.SessionId; } + get { return UserData.SessionId; } } - //public int SessionTimeout - //{ - // get - // { - // EnsureDeserialized(); - // return DeserializedData.SessionTimeout; - // } - //} - public string[] Roles { - get { return DeserializedData.Roles; } + get { return UserData.Roles; } } /// - /// This will ensure we only deserialize once + /// Gets a copy of the current instance. /// - /// - /// For performance reasons, we'll also check if there's an http context available, - /// if so, we'll chuck our instance in there so that we only deserialize once per request. - /// - protected void EnsureDeserialized() + /// + /// A copy of the current instance. + /// + public override ClaimsIdentity Clone() { - if (DeserializedData != null) - return; - - if (HttpContext.Current != null) - { - //check if we've already done this in this request - var data = HttpContext.Current.Items[typeof(UmbracoBackOfficeIdentity)] as UserData; - if (data != null) - { - DeserializedData = data; - return; - } - } - - if (string.IsNullOrEmpty(UserData)) - { - throw new NullReferenceException("The " + typeof(UserData) + " found in the ticket cannot be empty"); - } - DeserializedData = JsonConvert.DeserializeObject(UserData); - - if (HttpContext.Current != null) - { - HttpContext.Current.Items[typeof (UmbracoBackOfficeIdentity)] = DeserializedData; - } + return new UmbracoBackOfficeIdentity(this); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9579bbba59..c5da56af3d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,9 +54,25 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + True + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -326,6 +342,10 @@ + + + + @@ -343,6 +363,7 @@ + @@ -387,6 +408,7 @@ + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index c13e24ef1a..559eae83c4 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -3,6 +3,8 @@ + + @@ -11,6 +13,9 @@ + + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index e215fd552c..ba6e3b3d59 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -50,9 +50,11 @@ namespace Umbraco.Web.UI //Ensure owin is configured for Umbraco back office authentication - this must // be configured AFTER the standard UseCookieConfiguration above. - app.UseUmbracoBackAuthentication(); + app + .UseUmbracoBackOfficeCookieAuthentication() + .UseUmbracoBackOfficeExternalCookieAuthentication(); - app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 52eadd98da..d408e7a98a 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -49,7 +51,7 @@ namespace Umbraco.Web.Security.Identity ///

public abstract class MembershipProviderBase : MembershipProvider { + + public string HashPasswordForStorage(string password) + { + string salt; + var hashed = EncryptOrHashNewPassword(password, out salt); + return FormatPasswordForStorage(hashed, salt); + } + + public bool VerifyPassword(string password, string hashedPassword) + { + return CheckPassword(password, hashedPassword); + } + /// /// Providers can override this setting, default is 7 /// diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index f59f4d1169..bdd3174960 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -13,21 +13,21 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Security { - internal static class MembershipProviderExtensions + public static class MembershipProviderExtensions { - public static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) + internal static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) { int totalRecords = 0; return provider.FindUsersByName(usernameToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) + internal static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) { int totalRecords = 0; return provider.FindUsersByEmail(emailToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) + internal static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) { MembershipCreateStatus status; var user = provider.CreateUser(username, password, email, null, null, true, null, out status); @@ -80,7 +80,7 @@ namespace Umbraco.Core.Security ///
/// /// - public static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) + internal static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) { var username = membershipProvider.GetCurrentUserName(); return username.IsNullOrWhiteSpace() @@ -93,7 +93,7 @@ namespace Umbraco.Core.Security /// /// /// - public static string GetCurrentUserName(this MembershipProvider membershipProvider) + internal static string GetCurrentUserName(this MembershipProvider membershipProvider) { if (HostingEnvironment.IsHosted) { diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index be6811ad7b..08f53fb76e 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -27,8 +27,28 @@ namespace Umbraco.Core.Security //This just creates a temp/fake ticket : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) { + if (userdata == null) throw new ArgumentNullException("userdata"); UserData = userdata; - AddClaims(); + AddUserDataClaims(); + } + + /// + /// Create a back office identity based on an existing claims identity + /// + /// + /// + public UmbracoBackOfficeIdentity(ClaimsIdentity claimsIdentity, UserData userdata) + //This just creates a temp/fake ticket + : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) + { + if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity"); + if (userdata == null) throw new ArgumentNullException("userdata"); + + _currentIssuer = claimsIdentity.AuthenticationType; + UserData = userdata; + AddClaims(claimsIdentity); + Actor = claimsIdentity; + AddUserDataClaims(); } /// @@ -39,7 +59,7 @@ namespace Umbraco.Core.Security : base(ticket) { UserData = JsonConvert.DeserializeObject(ticket.UserData); - AddClaims(); + AddUserDataClaims(); } /// @@ -49,24 +69,49 @@ namespace Umbraco.Core.Security private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity) : base(identity) { + if (identity.Actor != null) + { + _currentIssuer = identity.AuthenticationType; + AddClaims(identity); + Actor = identity.Clone(); + } + UserData = identity.UserData; - AddClaims(); + AddUserDataClaims(); } - public static string Issuer = "UmbracoBackOffice"; + public const string Issuer = "UmbracoBackOffice"; + private readonly string _currentIssuer = Issuer; - //TODO: Another option is to create a ClaimsIdentityFactory when everything is wired up... optional though i think - private void AddClaims() + private void AddClaims(ClaimsIdentity claimsIdentity) + { + foreach (var claim in claimsIdentity.Claims) + { + AddClaim(claim); + } + } + + /// + /// Adds claims based on the UserData data + /// + private void AddUserDataClaims() { AddClaims(new[] { new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.AllowedApplicationsClaimType, string.Join(",", AllowedApplications), null, Issuer, Issuer, this), + + //TODO: Similar one created by the ClaimsIdentityFactory not sure we need this new Claim(Constants.Security.UserIdClaimType, Id.ToString(), null, Issuer, Issuer, this), new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this), new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this), - new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) + + //TODO: Role claims are added by the default ClaimsIdentityFactory based on the result from + // the user manager manager.GetRolesAsync method so not sure if we can do that there or needs to be done here + // and each role should be a different claim, not a single string + + //new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) }); } @@ -80,7 +125,7 @@ namespace Umbraco.Core.Security /// public override string AuthenticationType { - get { return Issuer; } + get { return _currentIssuer; } } public int StartContentNode diff --git a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs index 12ca2b4b4a..328d8ad335 100644 --- a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Security /// public abstract class UmbracoMembershipProviderBase : MembershipProviderBase { + + public abstract string DefaultMemberTypeAlias { get; } /// diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs new file mode 100644 index 0000000000..f33f1c492b --- /dev/null +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + public class ExternalLoginService : RepositoryService, IExternalLoginService + { + public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger) + : base(provider, repositoryFactory, logger) + { + } + + /// + /// Returns all user logins assigned + /// + /// + /// + public IEnumerable GetAll(int userId) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query().Where(x => x.UserId == userId)); + } + } + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + public IEnumerable Find(UserLoginInfo login) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query() + .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + } + } + + /// + /// Save user logins + /// + /// + /// + public void SaveUserLogins(int userId, IEnumerable logins) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.SaveUserLogins(userId, logins); + uow.Commit(); + } + } + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + public void DeleteUserLogins(int userId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.DeleteUserLogins(userId); + uow.Commit(); + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs new file mode 100644 index 0000000000..e1b1a161d8 --- /dev/null +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Services +{ + /// + /// Used to store the external login info, this can be replaced with your own implementation + /// + public interface IExternalLoginService : IService + { + /// + /// Returns all user logins assigned + /// + /// + /// + IEnumerable GetAll(int userId); + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + IEnumerable Find(UserLoginInfo login); + + /// + /// Save user logins + /// + /// + /// + void SaveUserLogins(int userId, IEnumerable logins); + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + void DeleteUserLogins(int userId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 484226ae78..7803abe98e 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Core.Models; diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f1eb873db7..d72115e49d 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -41,6 +41,7 @@ namespace Umbraco.Core.Services private Lazy _memberTypeService; private Lazy _memberGroupService; private Lazy _notificationService; + private Lazy _externalLoginService; /// /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used @@ -91,8 +92,10 @@ namespace Umbraco.Core.Services IDomainService domainService = null, ITaskService taskService = null, IMacroService macroService = null, - IPublicAccessService publicAccessService = null) + IPublicAccessService publicAccessService = null, + IExternalLoginService externalLoginService = null) { + if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); if (auditService != null) _auditService = new Lazy(() => auditService); if (localizedTextService != null) _localizedTextService = new Lazy(() => localizedTextService); if (tagService != null) _tagService = new Lazy(() => tagService); @@ -145,6 +148,9 @@ namespace Umbraco.Core.Services var provider = dbUnitOfWorkProvider; var fileProvider = fileUnitOfWorkProvider; + if (_externalLoginService == null) + _externalLoginService = new Lazy(() => new ExternalLoginService(provider, repositoryFactory, logger)); + if (_publicAccessService == null) _publicAccessService = new Lazy(() => new PublicAccessService(provider, repositoryFactory, logger)); @@ -415,5 +421,9 @@ namespace Umbraco.Core.Services get { return _memberGroupService.Value; } } + public IExternalLoginService ExternalLoginService + { + get { return _externalLoginService.Value; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f0f09c499c..f76cb2667c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -67,12 +67,13 @@ False ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll - - ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + False + ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll - True + + False + ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll True @@ -347,6 +348,8 @@ + + @@ -368,11 +371,13 @@ + + @@ -400,8 +405,10 @@ + + @@ -409,7 +416,9 @@ + + @@ -420,8 +429,10 @@ + + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 93c9737158..e8702a60bc 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -14,8 +14,8 @@ - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 00d3cda1ab..04e6672b4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -17,9 +17,8 @@ $scope.errorMsg = ""; - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - - $scope.externalLogins = Umbraco.Sys.ServerVariables.externalLogins; + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = Umbraco.Sys.ServerVariables.externalLogins.providers; $scope.loginSubmit = function (login, password) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 5b73743772..b09c11bf18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -23,12 +23,12 @@
{{errorMsg}}
- +

-
+
- -

+ +
+ +

External login providers

+ +
+ {{error}} +
-
+
- - -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index 5979962128..c6531e6016 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -1,10 +1,12 @@ angular.module("umbraco") - .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService) { + .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource) { - $scope.user = userService.getCurrentUser(); $scope.history = historyService.getCurrent(); $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evtHandlers = []; evtHandlers.push(eventsService.on("historyService.add", function (e, args) { $scope.history = args.all; @@ -49,6 +51,20 @@ angular.module("umbraco") }, 1000, false); // 1 second, do NOT execute a global digest } + $scope.unlink = function(e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + var asdf = ";" + }, function(err) { + var asdf = err; + }); + } + //get the user userService.getCurrentUser().then(function (user) { $scope.user = user; @@ -57,6 +73,16 @@ angular.module("umbraco") $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; //set the timer updateTimeout(); + + //set the linked logins + for (var login in $scope.user.linkedLogins) { + var found = _.find($scope.externalLoginProviders, function(i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = $scope.user.linkedLogins[login]; + } + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index ff71cec524..c73de2bb3a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -1,48 +1,76 @@
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index baf56c1bb5..a11e071aa3 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -63,7 +63,9 @@ namespace Umbraco.Web.UI //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - + app.UseGoogleAuthentication( + clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", + clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); } diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index 55c28da1f3..c51e1c6ab9 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -1,4 +1,5 @@ -@using System.Net.Http +@using System.Collections +@using System.Net.Http @using System.Web.Mvc.Html @using Umbraco.Core @using ClientDependency.Core @@ -52,9 +53,13 @@ @{ var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() - .Select(p => new {authType = p.AuthenticationType, caption = p.Caption, + .Select(p => new + { + authType = p.AuthenticationType, + caption = p.Caption, //TODO: Need to see if this exposes any sensitive data! - properties = p.Properties}) + properties = p.Properties + }) .ToArray(); } @@ -63,23 +68,42 @@ we will load the rest of the server vars after the user is authenticated. *@ + + + + @*And finally we can load in our angular app*@ @@ -104,4 +128,3 @@ - diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index e3f13d46f3..f6fcb2c0e7 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Claims; using System.Text; +using System.Threading.Tasks; using System.Web; using System.Web.Helpers; using System.Web.Http; @@ -24,6 +27,9 @@ using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using umbraco.providers; +using Microsoft.AspNet.Identity.Owin; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Editors { @@ -58,6 +64,54 @@ namespace Umbraco.Web.Editors throw new NotSupportedException("An HttpContext is required for this request"); } + [WebApi.UmbracoAuthorize] + [ValidateAngularAntiForgeryToken] + public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) + { + var result = await UserManager.RemoveLoginAsync( + User.Identity.GetUserId(), + new UserLoginInfo(unlinkLoginModel.LoginProvider, unlinkLoginModel.ProviderKey)); + + if (result.Succeeded) + { + var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); + await SignInAsync(user, isPersistent: false); + return Request.CreateResponse(HttpStatusCode.OK); + } + else + { + AddModelErrors(result); + return Request.CreateValidationErrorResponse(ModelState); + } + } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(prefix, error); + } + } + + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) + { + var owinContext = TryGetOwinContext().Result; + + owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + + owinContext.Authentication.SignIn( + new AuthenticationProperties() { IsPersistent = isPersistent }, + await GenerateUserIdentityAsync(user)); + } + + private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } + /// /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) /// @@ -85,7 +139,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - public UserDetail GetCurrentUser() + public async Task GetCurrentUser() { var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); var result = Mapper.Map(user); @@ -95,9 +149,23 @@ namespace Umbraco.Web.Editors //set their remaining seconds result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); } + + //now we need to fill in the user's linked logins, we can't do this in the mapper because it has no access to the + // user manager + + var identityUser = await UserManager.FindByIdAsync(user.Id); + result.LinkedLogins = identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + return result; } + private BackOfficeUserManager _userManager; + + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } + } + /// /// Logs a user in /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 2ee30bc632..47866de5c9 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -52,11 +52,6 @@ namespace Umbraco.Web.Editors { private BackOfficeUserManager _userManager; - protected IOwinContext OwinContext - { - get { return Request.GetOwinContext(); } - } - protected BackOfficeUserManager UserManager { get { return _userManager ?? (_userManager = OwinContext.GetUserManager()); } @@ -70,30 +65,25 @@ namespace Umbraco.Web.Editors { ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; + //check if there's errors in the TempData, assign to view bag and render the view + if (TempData["ExternalSignInError"] != null) + { + ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); + } + //First check if there's external login info, if there's not proceed as normal var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( Core.Constants.Security.BackOfficeExternalAuthenticationType); - + if (loginInfo == null) { return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } - // Sign in the user with this external login provider if the user already has a login - var user = await UserManager.FindAsync(loginInfo.Login); - if (user != null) - { - await SignInAsync(user, isPersistent: false); - //all signed in so just render the view as per normal - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); - } + //we're just logging in with an external source, not linking accounts + return await ExternalSignInAsync(loginInfo); - //The user hasn't used this login provider so need to display an error, they must link the provider in the user section - // TODO: Or wherever we decide to put that. - - // TODO: Return a real error in one way or another, maybe a different view? - - throw new SecurityNegotiationException("The requested provider " + loginInfo.Login.LoginProvider + " has not been linked to to an account"); } /// @@ -244,6 +234,8 @@ namespace Umbraco.Web.Editors { "umbracoUrls", new Dictionary { + {"externalLoginsUrl", Url.Action("ExternalLogin", "BackOffice")}, + {"externalLinkLoginsUrl", Url.Action("LinkLogin", "BackOffice")}, {"legacyTreeJs", Url.Action("LegacyTreeJs", "BackOffice")}, {"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")}, {"gridConfig", Url.Action("GetGridConfig", "BackOffice")}, @@ -326,10 +318,10 @@ namespace Umbraco.Web.Editors controller => controller.Fetch(string.Empty)) }, { - "relationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetById(0)) - }, - { + "relationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0)) + }, + { "rteApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetConfiguration()) }, @@ -395,7 +387,24 @@ namespace Umbraco.Web.Editors } }, {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, - {"application", GetApplicationState()} + { + "application", GetApplicationState() + }, + { + "externalLogins", new Dictionary + { + { + "providers", HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Select(p => new + { + authType = p.AuthenticationType, caption = p.Caption, + //TODO: Need to see if this exposes any sensitive data! + properties = p.Properties + }) + .ToArray() + } + } + } }; //cache the result if debugging is disabled @@ -410,22 +419,78 @@ namespace Umbraco.Web.Editors } [HttpPost] - [AllowAnonymous] public ActionResult ExternalLogin(string provider) { // Request a redirect to the external login provider return new ChallengeResult(provider, - Url.Action("Default", "BackOffice", new + Url.Action("Default", "BackOffice")); + } + + [UmbracoAuthorize] + [HttpPost] + public ActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + return new ChallengeResult(provider, + Url.Action("ExternalLinkLoginCallback", "BackOffice"), + User.Identity.GetUserId()); + } + + + + [HttpGet] + public async Task ExternalLinkLoginCallback() + { + var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync( + Core.Constants.Security.BackOfficeExternalAuthenticationType, + XsrfKey, User.Identity.GetUserId()); + + if (loginInfo == null) + { + //Add error and redirect for it to be displayed + TempData["ExternalSignInError"] = new[] { "An error occurred, could not get external login info" }; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); + if (result.Succeeded) + { + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + //Add errors and redirect for it to be displayed + TempData["ExternalSignInError"] = result.Errors; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo) + { + if (loginInfo == null) throw new ArgumentNullException("loginInfo"); + + // Sign in the user with this external login provider if the user already has a login + var user = await UserManager.FindAsync(loginInfo.Login); + if (user != null) + { + //sign in + await SignInAsync(user, isPersistent: false); + } + else + { + ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + + //Remove the cookie otherwise this message will keep appearing + if (Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType] != null) { - area = GlobalSettings.UmbracoMvcArea - })); + Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType].Expires = DateTime.MinValue; + } + } + + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) { - //TODO: I don't think we want to reference the 'default' external cookie since people might be using this on the front-end - // we'll need to create a secondary custom handler for the external cookie for the back office - OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); + OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); OwinContext.Authentication.SignIn( new AuthenticationProperties() {IsPersistent = isPersistent}, @@ -439,6 +504,11 @@ namespace Umbraco.Web.Editors var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); return userIdentity; } + + private IAuthenticationManager AuthenticationManager + { + get { return OwinContext.Authentication; } + } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index f32a379218..0d9d859b3d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -42,5 +42,8 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } + + [DataMember(Name = "linkedLogins")] + public IEnumerable> LinkedLogins { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/UnLinkLoginModel.cs b/src/Umbraco.Web/Models/UnLinkLoginModel.cs new file mode 100644 index 0000000000..776baf03fc --- /dev/null +++ b/src/Umbraco.Web/Models/UnLinkLoginModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + public class UnLinkLoginModel + { + [Required] + [DataMember(Name = "loginProvider", IsRequired = true)] + public string LoginProvider { get; set; } + + [Required] + [DataMember(Name = "providerKey", IsRequired = true)] + public string ProviderKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoController.cs b/src/Umbraco.Web/Mvc/UmbracoController.cs index 1ab13cedac..241d22b16e 100644 --- a/src/Umbraco.Web/Mvc/UmbracoController.cs +++ b/src/Umbraco.Web/Mvc/UmbracoController.cs @@ -1,5 +1,7 @@ using System; +using System.Web; using System.Web.Mvc; +using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Services; @@ -24,6 +26,11 @@ namespace Umbraco.Web.Mvc } + protected IOwinContext OwinContext + { + get { return Request.GetOwinContext(); } + } + private UmbracoHelper _umbraco; /// diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs index 52f50418e9..0acd002d42 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs @@ -36,6 +36,38 @@ namespace Umbraco.Web.Security.Identity }; } + /// + /// Extracts login info out of an external identity + /// + /// + /// + /// key that will be used to find the userId to verify + /// + /// the value expected to be found using the xsrfKey in the AuthenticationResult.Properties + /// dictionary + /// + /// + public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, + string authenticationType, + string xsrfKey, string expectedValue) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + var result = await manager.AuthenticateAsync(authenticationType); + // Verify that the userId is the same as what we expect if requested + if (result != null && + result.Properties != null && + result.Properties.Dictionary != null && + result.Properties.Dictionary.ContainsKey(xsrfKey) && + result.Properties.Dictionary[xsrfKey] == expectedValue) + { + return GetExternalLoginInfo(result); + } + return null; + } + /// /// Extracts login info out of an external identity /// diff --git a/src/Umbraco.Web/UI/JavaScript/Main.js b/src/Umbraco.Web/UI/JavaScript/Main.js index 520620af7d..afc4706ca3 100644 --- a/src/Umbraco.Web/UI/JavaScript/Main.js +++ b/src/Umbraco.Web/UI/JavaScript/Main.js @@ -3,6 +3,8 @@ LazyLoad.js("##JsInitialize##", function () { UmbClientMgr.setUmbracoPath('"##UmbracoPath##"'); jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); }); \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f40dc7bbd1..1edac1acb3 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -316,6 +316,7 @@ + diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs index 8eeada541e..bd2290ce17 100644 --- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; +using Microsoft.Owin; using Umbraco.Core; namespace Umbraco.Web.WebApi @@ -16,6 +17,20 @@ namespace Umbraco.Web.WebApi public static class HttpRequestMessageExtensions { + + /// + /// Borrowed from the latest Microsoft.AspNet.WebApi.Owin package which we cannot use because of a later webapi dependency + /// + /// + /// + internal static Attempt TryGetOwinContext(this HttpRequestMessage request) + { + var httpContext = request.TryGetHttpContext(); + return httpContext + ? Attempt.Succeed(httpContext.Result.GetOwinContext()) + : Attempt.Fail(); + } + /// /// Tries to retrieve the current HttpContext if one exists. /// diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index 9b1a37f4f7..33df591015 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; +using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -42,6 +43,15 @@ namespace Umbraco.Web.WebApi return Request.TryGetHttpContext(); } + /// + /// Tries to retrieve the current HttpContext if one exists. + /// + /// + protected Attempt TryGetOwinContext() + { + return Request.TryGetOwinContext(); + } + /// /// Returns an ILogger /// From a2a8c8fbd72603935d26edf31678aea149f7f98b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Feb 2015 17:26:28 +0100 Subject: [PATCH 146/249] updated to latest owin --- src/SQLCE4Umbraco/app.config | 16 +++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 11 +++++---- src/Umbraco.Core/app.config | 16 +++++++++++++ src/Umbraco.Core/packages.config | 4 ++-- src/Umbraco.Tests/App.config | 24 +++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 +++++----- src/Umbraco.Web.UI/packages.config | 4 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 8 +++---- src/Umbraco.Web/app.config | 12 ++++++++++ src/Umbraco.Web/packages.config | 4 ++-- src/UmbracoExamine/app.config | 16 +++++++++++++ src/umbraco.MacroEngines/app.config | 12 ++++++++++ src/umbraco.businesslogic/app.config | 16 +++++++++++++ src/umbraco.businesslogic/packages.config | 2 +- .../umbraco.businesslogic.csproj | 5 ++-- src/umbraco.cms/app.config | 16 +++++++++++++ src/umbraco.controls/app.config | 16 +++++++++++++ src/umbraco.datalayer/app.config | 12 ++++++++++ src/umbraco.editorControls/app.config | 12 ++++++++++ src/umbraco.macroRenderings/app.config | 16 +++++++++++++ src/umbraco.providers/app.config | 12 ++++++++++ 21 files changed, 222 insertions(+), 24 deletions(-) diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index 8f828418f3..777017ce39 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bd32b08677..85fe6d55bc 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -60,12 +60,13 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - + False - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + True False diff --git a/src/Umbraco.Core/app.config b/src/Umbraco.Core/app.config index 8f828418f3..777017ce39 100644 --- a/src/Umbraco.Core/app.config +++ b/src/Umbraco.Core/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index e8702a60bc..fd2be29ee3 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -12,8 +12,8 @@ - - + + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 86e6a0cbfe..893dd3c33f 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -156,6 +156,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cbc8c23880..f84cdc7f82 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,17 +158,17 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - False - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True False ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll - True + + False + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index c55244e7bf..f60d068561 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -23,9 +23,9 @@ - + - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1edac1acb3..d409c220ec 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,16 +138,16 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - + False - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - + False - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll False diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index 051211ebf7..fdd47d8fc1 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -53,6 +53,18 @@ + + + + + + + + + + + + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 1429ad68d5..318ccab708 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -19,9 +19,9 @@ - + - + diff --git a/src/UmbracoExamine/app.config b/src/UmbracoExamine/app.config index b77bae14a4..e25336af02 100644 --- a/src/UmbracoExamine/app.config +++ b/src/UmbracoExamine/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index a3f8757270..9176f3e6f5 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -28,6 +28,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.businesslogic/app.config b/src/umbraco.businesslogic/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.businesslogic/app.config +++ b/src/umbraco.businesslogic/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/packages.config b/src/umbraco.businesslogic/packages.config index 8ae1655c0d..b1c4140b1a 100644 --- a/src/umbraco.businesslogic/packages.config +++ b/src/umbraco.businesslogic/packages.config @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 7accd2ccb5..fdc3b405ad 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -113,8 +113,9 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + False + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True diff --git a/src/umbraco.cms/app.config b/src/umbraco.cms/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.cms/app.config +++ b/src/umbraco.cms/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.controls/app.config b/src/umbraco.controls/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.controls/app.config +++ b/src/umbraco.controls/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 6acdd4b4ca..777017ce39 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -16,6 +16,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 743b7c93ca..38ef813341 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -33,6 +33,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.macroRenderings/app.config b/src/umbraco.macroRenderings/app.config index e72c720717..ed1446828c 100644 --- a/src/umbraco.macroRenderings/app.config +++ b/src/umbraco.macroRenderings/app.config @@ -10,6 +10,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index 267e89a2dd..e25336af02 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -16,6 +16,18 @@ + + + + + + + + + + + + From afa4c7b697f6e5e98f99da9879350be95b8bd3a3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Feb 2015 18:51:44 +0100 Subject: [PATCH 147/249] open id connect is working with azure ad --- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 135 ++++++++++++++++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 18 +++ src/Umbraco.Web.UI/packages.config | 4 + .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 6 + src/Umbraco.Web/packages.config | 1 + 6 files changed, 153 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index a11e071aa3..a4eb25b341 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; using System.Web; -using System.Web.Security; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; +using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin; -using Microsoft.Owin.Security.Cookies; -using Microsoft.Owin.Security.Google; -using Umbraco.Web.Security.Identity; +using Microsoft.Owin.Security.OpenIdConnect; using Owin; using Umbraco.Core; using Umbraco.Core.Security; @@ -26,8 +23,23 @@ namespace Umbraco.Web.UI public class OwinStartup { + public async Task DoStuff() + { + var client = new HttpClient(); + + using (var request = await client.PostAsJsonAsync("", "123")) + { + + } + } + public void Configuration(IAppBuilder app) { + + + + + //Single method to configure the Identity user manager for use with Umbraco app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, @@ -63,12 +75,111 @@ namespace Umbraco.Web.UI //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - app.UseGoogleAuthentication( - clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", - clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); - + + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); + app.UseOpenIdConnectAuthentication( + new OpenIdConnectAuthenticationOptions + { + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + + Notifications = new OpenIdConnectAuthenticationNotifications() + { + // + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + // + AuthorizationCodeReceived = (context) => + { + var code = context.Code; + + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( + code, + //new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + + return Task.FromResult(0); + } + + } + + }); + } } + + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + string UserObjectId = string.Empty; + string CacheId = string.Empty; + public NaiveSessionCache(string userId) + { + UserObjectId = userId; + CacheId = UserObjectId + "_TokenCache"; + + this.AfterAccess = AfterAccessNotification; + this.BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[CacheId] = this.Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + this.HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + System.Web.HttpContext.Current.Session.Remove(CacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (this.HasStateChanged) + { + Persist(); + } + } + } + } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f84cdc7f82..52287c9084 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,6 +158,16 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + + + False + ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.1\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -181,6 +191,9 @@ False ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + + ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll + ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -232,6 +245,11 @@ + + + False + ..\packages\System.IdentityModel.Tokens.Jwt.4.0.1\lib\net45\System.IdentityModel.Tokens.Jwt.dll + False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index f60d068561..d7e4304b96 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -22,6 +22,8 @@ + + @@ -29,6 +31,7 @@ + @@ -36,5 +39,6 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 47866de5c9..336e1dbfde 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -687,7 +687,7 @@ namespace Umbraco.Web.Editors //Ensure the forms auth module doesn't do a redirect! context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; - var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; if (UserId != null) { properties.Dictionary[XsrfKey] = UserId; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d409c220ec..0117715330 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,6 +138,12 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 318ccab708..6f8f81ffbc 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -18,6 +18,7 @@ + From d9cf9cee88dbf15e1657241210ae3c7a1a76b6fc Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 22 Feb 2015 13:29:00 +0100 Subject: [PATCH 148/249] Includes nice social buttons, updates styling on login and user panel, updates logic to un-link accounts --- src/Umbraco.Web.UI.Client/bower.json | 12 ++- .../src/common/resources/auth.resource.js | 10 ++ src/Umbraco.Web.UI.Client/src/less/panel.less | 9 +- .../src/views/common/dialogs/login.html | 9 +- .../views/common/dialogs/user.controller.js | 57 +++++----- .../src/views/common/dialogs/user.html | 31 +++--- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 100 +++++++++++------- .../umbraco/Views/Default.cshtml | 9 +- .../Editors/AuthenticationController.cs | 16 +-- .../Models/ContentEditing/UserDetail.cs | 3 +- 10 files changed, 165 insertions(+), 91 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..ce312d669a 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -19,6 +19,7 @@ "typeahead.js": "~0.10.5", "underscore": "~1.7.0", "rgrove-lazyload": "*", + "bootstrap-social": "~4.8.0" "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", @@ -34,8 +35,15 @@ "underscore": { "": "underscore-min.{js,map}" }, - "angular-dynamic-locale": { - "": "tmhDynamicLocale.min.{js,js.map}" + "bootstrap-social": { + "": "bootstrap-social.css" + }, + "font-awesome": { + "css": "css/font-awesome.min.css", + "fonts" : "fonts/*" + }, + "bootstrap": { + "ignore": "*.ignore" }, "jquery": { "": "jquery.min.{js,map}" diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 973b841cdd..f32602bda6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -123,6 +123,16 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "GetCurrentUser")), 'Server call failed for getting current user'); }, + + getCurrentUserLinkedLogins: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "GetCurrentUserLinkedLogins")), + 'Server call failed for getting current users linked logins'); + }, /** * @ngdoc method diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7fc2d061ea..62a5d21ac7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -280,4 +280,11 @@ .umb-dialog a.text-success:hover, .umb-dialog a.text-success:focus, .umb-panel a.text-success:hover, -.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } \ No newline at end of file +.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } + +.umb-user-panel .external-logins form { + margin:0; +} +.umb-user-panel .external-logins button { + margin:5px; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 4981b4ca12..dd7971b678 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -36,11 +36,14 @@
- - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index c6531e6016..6048edfa95 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -51,7 +51,37 @@ angular.module("umbraco") }, 1000, false); // 1 second, do NOT execute a global digest } - $scope.unlink = function(e, loginProvider, providerKey) { + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm("Are you sure you want to unlink this account?"); if (!result) { e.preventDefault(); @@ -59,32 +89,11 @@ angular.module("umbraco") } authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - var asdf = ";" - }, function(err) { - var asdf = err; + updateUserInfo(); }); } - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - //set the linked logins - for (var login in $scope.user.linkedLogins) { - var found = _.find($scope.externalLoginProviders, function(i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = $scope.user.linkedLogins[login]; - } - } - } - }); + updateUserInfo(); //remove all event handlers $scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index c73de2bb3a..37751bfab6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -1,4 +1,4 @@ -
+
@@ -30,30 +30,35 @@

-
+
External login providers
-
+
-
- + value="{{login.authType}}"> + + Link your {{login.caption}} account +
- - {{login.caption}} - + value="{{login.authType}}"> + + Un-link your {{login.caption}} account +
diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index a4eb25b341..930aed24b0 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web; using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin; +using Microsoft.Owin.Security.Google; using Microsoft.Owin.Security.OpenIdConnect; using Owin; using Umbraco.Core; @@ -73,59 +74,84 @@ namespace Umbraco.Web.UI .UseUmbracoBackOfficeExternalCookieAuthentication(); //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - - + //app.UseGoogleAuthentication( + // clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", + // clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); + + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + + }; + googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; + googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; + googleOptions.Caption = "Google"; + app.UseGoogleAuthentication(googleOptions); + + //AD docs are here: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - app.UseOpenIdConnectAuthentication( - new OpenIdConnectAuthenticationOptions + var adOptions = new OpenIdConnectAuthenticationOptions + { + //NOTE: This by default is 'OpenIdConnect' but that doesn't match what identity actually stores in the + // loginProvider field in the database which is something like: https://sts.windows.net/1234.... + // which is something based on your AD setup. This value needs to match in order for accounts to detected as linked/un-linked + // in the back office. + AuthenticationType = "https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ddd/", + + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + Notifications = new OpenIdConnectAuthenticationNotifications() { - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - - Notifications = new OpenIdConnectAuthenticationNotifications() + // + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + // + AuthorizationCodeReceived = (context) => { - // - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - // - AuthorizationCodeReceived = (context) => - { - var code = context.Code; + var code = context.Code; - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( - code, - //new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( + code, + //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD + // configuration. + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + return Task.FromResult(0); } - }); + } + + }; + adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + adOptions.Description.Properties["SocialIcon"] = "fa-windows"; + adOptions.Caption = "Active Directory"; + app.UseOpenIdConnectAuthentication(adOptions); } } + //NOTE: Not sure exactly what this is for but it is found in the AD source demo: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs public class NaiveSessionCache : TokenCache { private static readonly object FileLock = new object(); - string UserObjectId = string.Empty; - string CacheId = string.Empty; + readonly string _userObjectId = string.Empty; + readonly string _cacheId = string.Empty; public NaiveSessionCache(string userId) { - UserObjectId = userId; - CacheId = UserObjectId + "_TokenCache"; + _userObjectId = userId; + _cacheId = _userObjectId + "_TokenCache"; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; @@ -136,7 +162,7 @@ namespace Umbraco.Web.UI { lock (FileLock) { - this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); + this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); } } @@ -145,7 +171,7 @@ namespace Umbraco.Web.UI lock (FileLock) { // reflect changes in the persistent store - HttpContext.Current.Session[CacheId] = this.Serialize(); + HttpContext.Current.Session[_cacheId] = this.Serialize(); // once the write operation took place, restore the HasStateChanged bit to false this.HasStateChanged = false; } @@ -155,7 +181,7 @@ namespace Umbraco.Web.UI public override void Clear() { base.Clear(); - System.Web.HttpContext.Current.Session.Remove(CacheId); + System.Web.HttpContext.Current.Session.Remove(_cacheId); } public override void DeleteItem(TokenCacheItem item) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index c51e1c6ab9..a392942fe1 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -28,8 +28,13 @@ Umbraco - @{ Html.RequiresCss("assets/css/umbraco.css", "Umbraco");} - @{ Html.RequiresCss("tree/treeicons.css", "UmbracoClient");} + @{ + Html + .RequiresCss("assets/css/umbraco.css", "Umbraco") + .RequiresCss("tree/treeicons.css", "UmbracoClient") + .RequiresCss("lib/bootstrap-social/bootstrap-social.css", "Umbraco") + .RequiresCss("lib/font-awesome/css/font-awesome.min.css", "Umbraco"); + } @Html.RenderCssHere( new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco)), new BasicPath("UmbracoClient", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index f6fcb2c0e7..a1e12401b1 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - public async Task GetCurrentUser() + public UserDetail GetCurrentUser() { var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); var result = Mapper.Map(user); @@ -150,15 +150,17 @@ namespace Umbraco.Web.Editors result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); } - //now we need to fill in the user's linked logins, we can't do this in the mapper because it has no access to the - // user manager - - var identityUser = await UserManager.FindByIdAsync(user.Id); - result.LinkedLogins = identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); - return result; } + [WebApi.UmbracoAuthorize] + [SetAngularAntiForgeryTokens] + public async Task> GetCurrentUserLinkedLogins() + { + var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); + return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + } + private BackOfficeUserManager _userManager; protected BackOfficeUserManager UserManager diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 0d9d859b3d..d27736576f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -43,7 +43,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } - [DataMember(Name = "linkedLogins")] - public IEnumerable> LinkedLogins { get; set; } + } } \ No newline at end of file From 4b156ba27e939003f076e818423021faebcb60de Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 22 Feb 2015 15:10:14 +0100 Subject: [PATCH 149/249] Starts stubbing out role manager code --- .../Models/Identity/BackOfficeIdentityRole.cs | 22 ++++++ .../Security/BackOfficeRoleManager.cs | 27 ++++++++ .../Security/BackOfficeRoleStore.cs | 67 +++++++++++++++++++ .../Security/BackOfficeUserManager.cs | 37 +++------- .../Security/BackOfficeUserStore.cs | 18 ++++- ...> IUmbracoMemberTypeMembershipProvider.cs} | 0 .../Security/MembershipPasswordHasher.cs | 31 +++++++++ src/Umbraco.Core/Services/IUserService.cs | 1 + src/Umbraco.Core/Umbraco.Core.csproj | 6 +- .../Security/Identity/AppBuilderExtensions.cs | 18 ++--- 10 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeRoleManager.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeRoleStore.cs rename src/Umbraco.Core/Security/{UmbracoMembersMembershipProviderBase.cs => IUmbracoMemberTypeMembershipProvider.cs} (100%) create mode 100644 src/Umbraco.Core/Security/MembershipPasswordHasher.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs new file mode 100644 index 0000000000..9fb4c51f52 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Models.Identity +{ + public class BackOfficeIdentityRole : IRole + { + public BackOfficeIdentityRole(string id) + { + Id = id; + } + + /// + /// Id of the role + /// + public string Id { get; private set; } + + /// + /// Name of the role + /// + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs new file mode 100644 index 0000000000..c49a6b943b --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeRoleManager : RoleManager + { + /// + /// Constructor + /// + /// The IRoleStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods + public BackOfficeRoleManager(IRoleStore store) : base(store) + { + } + + public static BackOfficeRoleManager Create( + IdentityFactoryOptions options) + { + //TODO: Set this up! + + var manager = new BackOfficeRoleManager(new BackOfficeRoleStore()); + + return manager; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs new file mode 100644 index 0000000000..603f2e1ed2 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeRoleStore : DisposableObject, IRoleStore + { + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + } + + /// + /// Create a new role + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Update a role + /// + /// + /// + public Task UpdateAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Delete a role + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Find a role by id + /// + /// + /// + public Task FindByIdAsync(string roleId) + { + throw new NotImplementedException(); + } + + /// + /// Find a role by name + /// + /// + /// + public Task FindByNameAsync(string roleName) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 9634b17c73..b65b9dfaa1 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -12,33 +12,6 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - /// - /// A custom password hasher that conforms to the current password hashing done in Umbraco - /// - internal class MembershipPasswordHasher : IPasswordHasher - { - private readonly MembershipProviderBase _provider; - - public MembershipPasswordHasher(MembershipProviderBase provider) - { - _provider = provider; - } - - public string HashPassword(string password) - { - return _provider.HashPasswordForStorage(password); - } - - public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) - { - return _provider.VerifyPassword(providedPassword, hashedPassword) - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - - - } - /// /// Back office user manager /// @@ -51,6 +24,12 @@ namespace Umbraco.Core.Security #region What we support currently + //NOTE: Not sure if we really want/need to ever support this + public override bool SupportsUserClaim + { + get { return false; } + } + //TODO: Support this public override bool SupportsUserRole { @@ -75,11 +54,13 @@ namespace Umbraco.Core.Security get { return false; } } + //TODO: Support this public override bool SupportsUserTwoFactor { get { return false; } } + //TODO: Support this public override bool SupportsUserPhoneNumber { get { return false; } @@ -127,7 +108,7 @@ namespace Umbraco.Core.Security } //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); //NOTE: Not implementing these currently diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 83676b9c1f..6dd4aadc3e 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -11,7 +11,23 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore + public class BackOfficeUserStore : DisposableObject, + IUserStore, + IUserPasswordStore, + IUserEmailStore, + IUserLoginStore + + //IUserRoleStore, + + //TODO: This will require additional columns/tables + //IUserLockoutStore + + //TODO: Implement this - might need to add a new column for this + // http://stackoverflow.com/questions/19487322/what-is-asp-net-identitys-iusersecuritystampstoretuser-interface + //IUserSecurityStampStore + + //TODO: To do this we need to implement IQueryable - seems pretty overkill? + //IQueryableUserStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; diff --git a/src/Umbraco.Core/Security/UmbracoMembersMembershipProviderBase.cs b/src/Umbraco.Core/Security/IUmbracoMemberTypeMembershipProvider.cs similarity index 100% rename from src/Umbraco.Core/Security/UmbracoMembersMembershipProviderBase.cs rename to src/Umbraco.Core/Security/IUmbracoMemberTypeMembershipProvider.cs diff --git a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs new file mode 100644 index 0000000000..56daa3efdd --- /dev/null +++ b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A custom password hasher that conforms to the current password hashing done in Umbraco + /// + internal class MembershipPasswordHasher : IPasswordHasher + { + private readonly MembershipProviderBase _provider; + + public MembershipPasswordHasher(MembershipProviderBase provider) + { + _provider = provider; + } + + public string HashPassword(string password) + { + return _provider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return _provider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 5a77956931..aa8250cf59 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 85fe6d55bc..0c2c0bee73 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,6 +345,7 @@ + @@ -419,8 +420,11 @@ + + + @@ -1144,7 +1148,7 @@ - + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index c1a6ed26a8..0e8712ed07 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -37,6 +37,9 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); + + //Configure Umbraco role manager to be created per request + app.CreatePerOwinContext((options, owinContext) => BackOfficeRoleManager.Create(options)); } /// @@ -70,14 +73,16 @@ namespace Umbraco.Web.Security.Identity return app; } + /// + /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct + /// Umbraco back office configuration + /// + /// + /// public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app) { if (app == null) throw new ArgumentNullException("app"); - //TODO: Figure out why this isn't working and is only working with the default one, must be a reference somewhere - - //app.UseExternalSignInCookie("UmbracoExternalCookie"); - app.SetDefaultSignInAsAuthenticationType("UmbracoExternalCookie"); app.UseCookieAuthentication(new CookieAuthenticationOptions { @@ -93,11 +98,6 @@ namespace Umbraco.Web.Security.Identity CookieDomain = UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain }); - - //NOTE: This works, but this is just the default implementation which we don't want because other devs - //might want to use this... right? - //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - return app; } From ff602da0fa3f230982e00ff3d8edb677c3ee94a7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 5 Mar 2015 14:02:39 +1100 Subject: [PATCH 150/249] Updates owin packages and updates the web.config tempate for owin redirects --- src/SQLCE4Umbraco/SqlCE4Umbraco.csproj | 5 +++-- src/SQLCE4Umbraco/app.config | 4 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 8 ++++---- src/Umbraco.Core/app.config | 4 ++-- src/Umbraco.Core/packages.config | 4 ++-- src/Umbraco.Tests/App.config | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 22 +++++++++++++--------- src/Umbraco.Web.UI/packages.config | 9 +++++---- src/Umbraco.Web.UI/web.Template.config | 17 +++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 13 +++++++------ src/Umbraco.Web/app.config | 4 ++-- src/Umbraco.Web/packages.config | 6 +++--- src/UmbracoExamine/app.config | 4 ++-- src/umbraco.MacroEngines/app.config | 4 ++-- src/umbraco.businesslogic/app.config | 4 ++-- src/umbraco.cms/app.config | 4 ++-- src/umbraco.controls/app.config | 4 ++-- src/umbraco.datalayer/app.config | 4 ++-- src/umbraco.editorControls/app.config | 4 ++-- src/umbraco.providers/app.config | 4 ++-- 20 files changed, 78 insertions(+), 54 deletions(-) diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index b7d3f1f655..73983e7e30 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -77,8 +77,9 @@ - - + + Designer + diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0c2c0bee73..52555fd9db 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -68,13 +68,13 @@ ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll True - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll True diff --git a/src/Umbraco.Core/app.config b/src/Umbraco.Core/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/Umbraco.Core/app.config +++ b/src/Umbraco.Core/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index fd2be29ee3..6281d1338f 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -14,8 +14,8 @@ - - + + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 893dd3c33f..f1917d16c0 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -164,7 +164,7 @@ - + @@ -180,7 +180,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 52287c9084..1392750d0c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -172,24 +172,28 @@ ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True - + False - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll False ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.Google.3.0.0\lib\net45\Microsoft.Owin.Security.Google.dll + + ..\packages\Microsoft.Owin.Security.Facebook.3.0.1\lib\net45\Microsoft.Owin.Security.Facebook.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll + + + False + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll @@ -2605,7 +2609,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7301 + http://localhost:7300 False False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index d7e4304b96..c0e0034b61 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -26,11 +26,12 @@ - + - - - + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 0d8400471f..b769cc7318 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -263,6 +263,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0117715330..537f7dd6b1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -148,20 +148,21 @@ False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + False + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll False ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll True diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index fdd47d8fc1..71898fd12e 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -57,7 +57,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 6f8f81ffbc..d1898c46fd 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -21,10 +21,10 @@ - + - - + + diff --git a/src/UmbracoExamine/app.config b/src/UmbracoExamine/app.config index e25336af02..4022c25600 100644 --- a/src/UmbracoExamine/app.config +++ b/src/UmbracoExamine/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index 9176f3e6f5..cabc84546e 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -32,7 +32,7 @@ - + @@ -40,7 +40,7 @@ - + diff --git a/src/umbraco.businesslogic/app.config b/src/umbraco.businesslogic/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.businesslogic/app.config +++ b/src/umbraco.businesslogic/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.cms/app.config b/src/umbraco.cms/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.cms/app.config +++ b/src/umbraco.cms/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.controls/app.config b/src/umbraco.controls/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.controls/app.config +++ b/src/umbraco.controls/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 38ef813341..68046c3af5 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -37,7 +37,7 @@ - + @@ -45,7 +45,7 @@ - + diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + From 7dc50fda260833d778ebee4817f1f7716b898d7c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 16:58:01 +1100 Subject: [PATCH 151/249] moves NaiveSessionCache to web proj --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../Security/Identity/NaiveSessionCache.cs | 84 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1392750d0c..4b8e33ca86 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2609,7 +2609,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7300 + http://localhost:7301 False False diff --git a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs new file mode 100644 index 0000000000..f4df1fbb30 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Umbraco.Web.Security.Identity +{ + //NOTE: Not sure exactly what this is for but it is found in the AD source demo: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs + // apparently it is needed for AD auth, so we'll put it here for people to use. + // It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs + // and please note that that link came from finding this thread: https://twitter.com/chakkaradeep/status/544962341528285184 + + /// + /// This is required to initialize the AD Identity provider on startup + /// + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + readonly string _userObjectId = string.Empty; + readonly string _cacheId = string.Empty; + public NaiveSessionCache(string userId) + { + _userObjectId = userId; + _cacheId = _userObjectId + "_TokenCache"; + + this.AfterAccess = AfterAccessNotification; + this.BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[_cacheId] = this.Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + this.HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + System.Web.HttpContext.Current.Session.Remove(_cacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (this.HasStateChanged) + { + Persist(); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 537f7dd6b1..79c59c85a4 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -560,6 +560,7 @@ + From 2d72a6687922ccc235f22571c93fbf0fd47b77c4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 12:50:31 +1100 Subject: [PATCH 152/249] Updates OwinStartup and split the methods into an extension methods file complete with documentation on how to implement the providers. Tested the microsoft provider. Now to clean things up: remove the 3rd party package installs to be ready for shipping, ensure that the user parts are extensible enough for people to plugin their own interfaces. --- src/Umbraco.Web.UI.Client/src/less/login.less | 82 ++++--- src/Umbraco.Web.UI.Client/src/less/panel.less | 4 +- .../src/views/common/dialogs/login.html | 50 +++-- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 208 +++--------------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + src/Umbraco.Web.UI/packages.config | 1 + .../Security/Identity/AppBuilderExtensions.cs | 10 +- .../Security/Identity/NaiveSessionCache.cs | 84 ------- src/Umbraco.Web/Umbraco.Web.csproj | 7 - src/Umbraco.Web/packages.config | 1 - 10 files changed, 122 insertions(+), 329 deletions(-) delete mode 100644 src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs diff --git a/src/Umbraco.Web.UI.Client/src/less/login.less b/src/Umbraco.Web.UI.Client/src/less/login.less index d46bb41317..d01dec8733 100644 --- a/src/Umbraco.Web.UI.Client/src/less/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/login.less @@ -2,46 +2,72 @@ // ------------------------- .login-overlay { - width: 100%; - height: 100%; - background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; - background-size: 30px 30px !important; - color: @white; - position: absolute; - z-index: 2000; - top: 0px; - left: 0px; - margin: 0 !Important; - padding: 0; - border-radius: 0 + width: 100%; + height: 100%; + background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; + background-size: 30px 30px !important; + color: @white; + position: absolute; + z-index: 2000; + top: 0px; + left: 0px; + margin: 0 !Important; + padding: 0; + border-radius: 0; } -.login-overlay .umb-modalcolumn{ - background: none; - border: none; +.login-overlay .umb-modalcolumn { + background: none; + border: none; } .login-overlay .form { - display: block; - padding-top: 100px; - padding-left: 165px; - width: 370px; - text-align: right + display: block; + padding-top: 100px; + padding-left: 165px; + width: 370px; + text-align: right; } .login-overlay h1 { - display: block; - text-align: right; - color: @white; - font-size: 18px; - font-weight: normal + display: block; + text-align: right; + color: @white; + font-size: 18px; + font-weight: normal; } -.login-overlay .alert.alert-error{ +.login-overlay .alert.alert-error { display: inline-block; - width: 270px; padding-right: 6px; padding-left: 6px; margin-top: 10px; text-align: center; -} \ No newline at end of file +} + +#hrOr { + height: 30px; + text-align: center; + position: relative; + padding-top: 20px; +} + +#hrOr hr { + margin: 0px; + border: none; + background-color: @gray; + height: 1px; +} + +#hrOr div { + background-color: black; + position: relative; + top: -16px; + border: 1px solid @gray; + padding: 4px; + border-radius: 50%; + width: 20px; + height: 20px; + margin: auto; + color: @grayLight; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 62a5d21ac7..c661fe1223 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -282,9 +282,9 @@ .umb-panel a.text-success:hover, .umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } -.umb-user-panel .external-logins form { +.external-logins form { margin:0; } -.umb-user-panel .external-logins button { +.external-logins button { margin:5px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index dd7971b678..25a8b28976 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -7,6 +7,33 @@ Log in below. Log in below

+ +
+ +
+ {{error}} +
+ +
+ +
+ + + +
+
+ +
+
Or
+
+ +
@@ -24,27 +51,6 @@
-
- -

External login providers

- -
- {{error}} -
- -
- -
- - - -
-
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 930aed24b0..431cc35786 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -1,12 +1,4 @@ -using System; -using System.Globalization; -using System.Net.Http; -using System.Threading.Tasks; -using System.Web; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.Owin; -using Microsoft.Owin.Security.Google; -using Microsoft.Owin.Security.OpenIdConnect; +using Microsoft.Owin; using Owin; using Umbraco.Core; using Umbraco.Core.Security; @@ -19,193 +11,47 @@ namespace Umbraco.Web.UI { /// - /// Summary description for Startup + /// Default OWIN startup class /// public class OwinStartup { - public async Task DoStuff() - { - var client = new HttpClient(); - - using (var request = await client.PostAsJsonAsync("", "123")) - { - - } - } - public void Configuration(IAppBuilder app) { - - - - - - //Single method to configure the Identity user manager for use with Umbraco + //Single method to configure the Identity user manager for use with Umbraco Back office app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - //// Enable the application to use a cookie to store information for the - //// signed in user and to use a cookie to temporarily store information - //// about a user logging in with a third party login provider - //// Configure the sign in cookie - //app.UseCookieAuthentication(new CookieAuthenticationOptions - //{ - // AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, - - // Provider = new CookieAuthenticationProvider - // { - // // Enables the application to validate the security stamp when the user - // // logs in. This is a security feature which is used when you - // // change a password or add an external login to your account. - // OnValidateIdentity = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) - // } - //}); - - //Ensure owin is configured for Umbraco back office authentication - this must - // be configured AFTER the standard UseCookieConfiguration above. + //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN + // cookie configuration, this must be declared after it. app .UseUmbracoBackOfficeCookieAuthentication() .UseUmbracoBackOfficeExternalCookieAuthentication(); - //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - - //app.UseGoogleAuthentication( - // clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", - // clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); + /* + * Configure external logins: + * + * Depending on the authentication sources you would like to enable, you will need to install + * certain Nuget packages. + * + * For Google auth: Install-Package Microsoft.Owin.Security.Google + * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook + * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount + * + * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can + * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services + * + * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment + * the methods that you would like to use. Each method contains documentation and links to + * documentation for reference. You can also tweak the code in those extension + * methods to suit your needs. + */ - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - - }; - googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; - googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; - googleOptions.Caption = "Google"; - app.UseGoogleAuthentication(googleOptions); - - //AD docs are here: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - - var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - var adOptions = new OpenIdConnectAuthenticationOptions - { - //NOTE: This by default is 'OpenIdConnect' but that doesn't match what identity actually stores in the - // loginProvider field in the database which is something like: https://sts.windows.net/1234.... - // which is something based on your AD setup. This value needs to match in order for accounts to detected as linked/un-linked - // in the back office. - AuthenticationType = "https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ddd/", - - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - Notifications = new OpenIdConnectAuthenticationNotifications() - { - // - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - // - AuthorizationCodeReceived = (context) => - { - var code = context.Code; - - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( - code, - //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD - // configuration. - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } - - } - - }; - adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - adOptions.Description.Properties["SocialIcon"] = "fa-windows"; - adOptions.Caption = "Active Directory"; - app.UseOpenIdConnectAuthentication(adOptions); - - } - - - } - - //NOTE: Not sure exactly what this is for but it is found in the AD source demo: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _userObjectId = string.Empty; - readonly string _cacheId = string.Empty; - public NaiveSessionCache(string userId) - { - _userObjectId = userId; - _cacheId = _userObjectId + "_TokenCache"; - - this.AfterAccess = AfterAccessNotification; - this.BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = this.Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - this.HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - System.Web.HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (this.HasStateChanged) - { - Persist(); - } + //app.ConfigureGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureActiveDirectory("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); } } - } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4b8e33ca86..0c74a63e4b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -191,6 +191,9 @@ False ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll + + ..\packages\Microsoft.Owin.Security.MicrosoftAccount.3.0.1\lib\net45\Microsoft.Owin.Security.MicrosoftAccount.dll + False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -380,6 +383,7 @@ Properties\SolutionInfo.cs + loadStarterKits.ascx ASPXCodeBehind diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index c0e0034b61..62c48ef30a 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -31,6 +31,7 @@ + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 0e8712ed07..67a3625f34 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.Security.Identity { public static class AppBuilderExtensions { + #region Backoffice /// /// Configure Identity User Manager for Umbraco /// @@ -32,8 +33,8 @@ namespace Umbraco.Web.Security.Identity //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( - options, - owinContext, + options, + owinContext, appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); @@ -58,7 +59,7 @@ namespace Umbraco.Web.Security.Identity GlobalSettings.UseSSL) { Provider = new CookieAuthenticationProvider - { + { //// Enables the application to validate the security stamp when the user //// logs in. This is a security feature which is used when you //// change a password or add an external login to your account. @@ -99,7 +100,8 @@ namespace Umbraco.Web.Security.Identity }); return app; - } + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs deleted file mode 100644 index f4df1fbb30..0000000000 --- a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using Microsoft.IdentityModel.Clients.ActiveDirectory; - -namespace Umbraco.Web.Security.Identity -{ - //NOTE: Not sure exactly what this is for but it is found in the AD source demo: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - // apparently it is needed for AD auth, so we'll put it here for people to use. - // It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs - // and please note that that link came from finding this thread: https://twitter.com/chakkaradeep/status/544962341528285184 - - /// - /// This is required to initialize the AD Identity provider on startup - /// - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _userObjectId = string.Empty; - readonly string _cacheId = string.Empty; - public NaiveSessionCache(string userId) - { - _userObjectId = userId; - _cacheId = _userObjectId + "_TokenCache"; - - this.AfterAccess = AfterAccessNotification; - this.BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = this.Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - this.HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - System.Web.HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (this.HasStateChanged) - { - Persist(); - } - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 79c59c85a4..0b8fea4a2f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,12 +138,6 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll - False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll @@ -560,7 +554,6 @@ - diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index d1898c46fd..bd20574668 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -18,7 +18,6 @@ - From 3efd038906b3b1f63291d0fc00ab55930fd51705 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:13:06 +1100 Subject: [PATCH 153/249] implements IUserRoleStore for sections for users --- .../Security/BackOfficeUserManager.cs | 10 +-- .../Security/BackOfficeUserStore.cs | 76 ++++++++++++++++++- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b65b9dfaa1..f84d333843 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Security { } - #region What we support currently + #region What we support do not currently //NOTE: Not sure if we really want/need to ever support this public override bool SupportsUserClaim @@ -30,12 +30,6 @@ namespace Umbraco.Core.Security get { return false; } } - //TODO: Support this - public override bool SupportsUserRole - { - get { return false; } - } - //TODO: Support this public override bool SupportsQueryableUsers { @@ -110,7 +104,7 @@ namespace Umbraco.Core.Security //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - //NOTE: Not implementing these currently + //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user //// You can write your own provider and plug in here. diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 6dd4aadc3e..dd2041fd51 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -15,9 +15,8 @@ namespace Umbraco.Core.Security IUserStore, IUserPasswordStore, IUserEmailStore, - IUserLoginStore - - //IUserRoleStore, + IUserLoginStore, + IUserRoleStore //TODO: This will require additional columns/tables //IUserLockoutStore @@ -442,5 +441,76 @@ namespace Umbraco.Core.Security return anythingChanged; } + /// + /// Adds a user to a role (section) + /// + /// + /// + public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + { + if (user.AllowedApplications.InvariantContains(roleName)) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.AddAllowedSection(roleName); + _userService.Save(found); + } + + return Task.FromResult(0); + } + + /// + /// Removes the role (allowed section) for the user + /// + /// + /// + public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) + { + if (user.AllowedApplications.InvariantContains(roleName) == false) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.RemoveAllowedSection(roleName); + _userService.Save(found); + } + + return Task.FromResult(0); + } + + /// + /// Returns the roles for this user + /// + /// + /// + public Task> GetRolesAsync(BackOfficeIdentityUser user) + { + return Task.FromResult((IList)user.AllowedApplications.ToList()); + } + + /// + /// Returns true if a user is in the role + /// + /// + /// + public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) + { + return Task.FromResult(user.AllowedApplications.InvariantContains(roleName)); + } } } \ No newline at end of file From b269760b21510f0a353a8f20063ed9bed7ba6da7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:16:32 +1100 Subject: [PATCH 154/249] removes the BackOfficeRoleManager since we don't use roles in the back office (sections i suppose) and we can't dynamically just create them, that doesn't make sense. --- .../Models/Identity/BackOfficeIdentityRole.cs | 22 ------ .../Security/BackOfficeRoleManager.cs | 27 -------- .../Security/BackOfficeRoleStore.cs | 67 ------------------- src/Umbraco.Core/Umbraco.Core.csproj | 3 - .../Security/Identity/AppBuilderExtensions.cs | 7 +- 5 files changed, 3 insertions(+), 123 deletions(-) delete mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs delete mode 100644 src/Umbraco.Core/Security/BackOfficeRoleManager.cs delete mode 100644 src/Umbraco.Core/Security/BackOfficeRoleStore.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs deleted file mode 100644 index 9fb4c51f52..0000000000 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNet.Identity; - -namespace Umbraco.Core.Models.Identity -{ - public class BackOfficeIdentityRole : IRole - { - public BackOfficeIdentityRole(string id) - { - Id = id; - } - - /// - /// Id of the role - /// - public string Id { get; private set; } - - /// - /// Name of the role - /// - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs deleted file mode 100644 index c49a6b943b..0000000000 --- a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.Security -{ - public class BackOfficeRoleManager : RoleManager - { - /// - /// Constructor - /// - /// The IRoleStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods - public BackOfficeRoleManager(IRoleStore store) : base(store) - { - } - - public static BackOfficeRoleManager Create( - IdentityFactoryOptions options) - { - //TODO: Set this up! - - var manager = new BackOfficeRoleManager(new BackOfficeRoleStore()); - - return manager; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs deleted file mode 100644 index 603f2e1ed2..0000000000 --- a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.Security -{ - public class BackOfficeRoleStore : DisposableObject, IRoleStore - { - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { - } - - /// - /// Create a new role - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Update a role - /// - /// - /// - public Task UpdateAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Delete a role - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Find a role by id - /// - /// - /// - public Task FindByIdAsync(string roleId) - { - throw new NotImplementedException(); - } - - /// - /// Find a role by name - /// - /// - /// - public Task FindByNameAsync(string roleName) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 52555fd9db..c62c869e4e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,7 +345,6 @@ - @@ -420,8 +419,6 @@ - - diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 67a3625f34..63d4efd2e6 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -23,7 +23,9 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, ApplicationContext appContext, MembershipProviderBase userMembershipProvider) + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + MembershipProviderBase userMembershipProvider) { //Don't proceed if the app is not ready if (appContext.IsConfigured == false @@ -38,9 +40,6 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); - - //Configure Umbraco role manager to be created per request - app.CreatePerOwinContext((options, owinContext) => BackOfficeRoleManager.Create(options)); } /// From 5a88ff774cc9178ab03047154ddc1d19da7cdb15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:36:52 +1100 Subject: [PATCH 155/249] adds overload to specify custom backoffice user store for custom implementations (i.e. 2 factor auth, etc...) --- .../Security/BackOfficeUserManager.cs | 44 +++++++++++++++++-- .../src/views/common/dialogs/user.html | 2 +- .../Security/Identity/AppBuilderExtensions.cs | 29 +++++++++++- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index f84d333843..e4c58ed6f0 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -61,20 +61,58 @@ namespace Umbraco.Core.Security } #endregion + /// + /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager + /// + /// + /// + /// + /// + /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, - IOwinContext context, IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase membershipProvider) { if (options == null) throw new ArgumentNullException("options"); - if (context == null) throw new ArgumentNullException("context"); if (userService == null) throw new ArgumentNullException("userService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); + return InitUserManager(manager, membershipProvider, options); + } + + /// + /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance + /// + /// + /// + /// + /// + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider) + { + if (options == null) throw new ArgumentNullException("options"); + if (customUserStore == null) throw new ArgumentNullException("customUserStore"); + + var manager = new BackOfficeUserManager(customUserStore); + + return InitUserManager(manager, membershipProvider, options); + } + + /// + /// Initializes the user manager with the correct options + /// + /// + /// + /// + /// + private static BackOfficeUserManager InitUserManager(BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) + { // Configure validation logic for usernames manager.UserValidator = new UserValidator(manager) { @@ -102,7 +140,7 @@ namespace Umbraco.Core.Security } //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index 37751bfab6..8346acfabb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -30,7 +30,7 @@

-
+
External login providers
diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 63d4efd2e6..912b19fd2e 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -17,8 +17,9 @@ namespace Umbraco.Web.Security.Identity public static class AppBuilderExtensions { #region Backoffice + /// - /// Configure Identity User Manager for Umbraco + /// Configure Default Identity User Manager for Umbraco /// /// /// @@ -36,12 +37,36 @@ namespace Umbraco.Web.Security.Identity app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( options, - owinContext, appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); } + /// + /// Configure a custom UserStore with the Identity User Manager for Umbraco + /// + /// + /// + /// + /// + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + MembershipProviderBase userMembershipProvider, + BackOfficeUserStore customUserStore) + { + //Don't proceed if the app is not ready + if (appContext.IsConfigured == false + || appContext.DatabaseContext == null + || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + //Configure Umbraco user manager to be created per request + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeUserManager.Create( + options, + customUserStore, + userMembershipProvider)); + } + /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// From 1f9594eef42200730fd5fca53b1aa42f7b8211a6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:38:05 +1100 Subject: [PATCH 156/249] updates notes in OwinStartup --- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 431cc35786..6d35ec5975 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -9,16 +9,15 @@ using Umbraco.Web.UI; namespace Umbraco.Web.UI { - /// /// Default OWIN startup class /// public class OwinStartup { - public void Configuration(IAppBuilder app) { - //Single method to configure the Identity user manager for use with Umbraco Back office + //Configure the Identity user manager for use with Umbraco Back office + // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); From 4d50dcea619fbf4f0e42734bdfe9ba22f1433a4b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:46:32 +1100 Subject: [PATCH 157/249] adds the auth extensions and removes all 3rd party packages --- .../App_Code/IdentityAuthExtensions.cs | 294 ++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 27 -- src/Umbraco.Web.UI/packages.config | 7 - 3 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs diff --git a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs new file mode 100644 index 0000000000..c3aaf9345d --- /dev/null +++ b/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +//using Microsoft.Owin.Security.MicrosoftAccount; +//using Microsoft.IdentityModel.Clients.ActiveDirectory; +//using Microsoft.Owin.Security.Facebook; +//using Microsoft.Owin.Security.Google; +//using Microsoft.Owin.Security.OpenIdConnect; + +namespace Umbraco.Web.UI +{ + public static class IdentityAuthExtensions + { + + /* + + /// + /// Configure microsoft account sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.MicrosoftAccount + /// + /// Microsoft account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT + /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx + /// + /// Microsoft apps can be created here: + /// + /// http://go.microsoft.com/fwlink/?LinkID=144070 + /// + /// + public static void ConfigureMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + { + var msOptions = new MicrosoftAccountAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }; + //Defines styles for buttons + msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + msOptions.Description.Properties["SocialIcon"] = "fa-windows"; + msOptions.Caption = "Microsoft"; + app.UseMicrosoftAccountAuthentication(msOptions); + } + + */ + + /* + + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google + /// + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + { + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }; + //Defines styles for buttons + googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; + googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; + googleOptions.Caption = "Google"; + app.UseGoogleAuthentication(googleOptions); + } + + */ + + /* + + /// + /// Configure facebook sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Facebook + /// + /// Facebook account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK + /// + /// Facebook apps can be created here: + /// + /// https://developers.facebook.com/ + /// + /// + public static void ConfigureFacebookAuth(this IAppBuilder app, string appId, string appSecret) + { + var fbOptions = new FacebookAuthenticationOptions + { + AppId = appId, + AppSecret = appSecret, + }; + //Defines styles for buttons + fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; + fbOptions.Description.Properties["SocialIcon"] = "fa-facebook"; + fbOptions.Caption = "Facebook"; + app.UseFacebookAuthentication(fbOptions); + } + + */ + + + /* + + /// + /// Configure ActiveDirectory sign-in + /// + /// + /// + /// + /// + /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; + /// + /// + /// + /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the + /// loginProvider field in the database which looks something like this (for example): + /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ + /// and is based on your AD setup. This value needs to match in order for accounts to + /// detected as linked/un-linked in the back office. + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.OpenIdConnect + /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory + /// + /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet + /// + /// This configuration requires the NaiveSessionCache class below which will need to be un-commented + /// + /// + public static void ConfigureActiveDirectory(this IAppBuilder app, + string tenant, string clientId, string postLoginRedirectUri, string appKey, + string authType) + { + const string aadInstance = "https://login.windows.net/{0}"; + const string graphResourceId = "https://graph.windows.net"; + + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); + var adOptions = new OpenIdConnectAuthenticationOptions + { + AuthenticationType = authType, + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + Notifications = new OpenIdConnectAuthenticationNotifications() + { + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + AuthorizationCodeReceived = (context) => + { + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + var result = authContext.AcquireTokenByAuthorizationCode( + context.Code, + //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD configuration. + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + + return Task.FromResult(0); + } + + } + + }; + adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + adOptions.Description.Properties["SocialIcon"] = "fa-windows"; + adOptions.Caption = "Active Directory"; + app.UseOpenIdConnectAuthentication(adOptions); + } + + */ + } + + /* + + /// + /// A Session cache token storage which is required to initialize the AD Identity provider on startup + /// + /// + /// Based on the examples from the AD samples: + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs + /// + /// There are some newer examples of different token storage including persistent storage here: + /// It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs + /// + /// The type of token storage will be dependent on your requirements but this should be fine for standard installations + /// + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + readonly string _cacheId; + public NaiveSessionCache(string userId) + { + _cacheId = userId + "_TokenCache"; + + AfterAccess = AfterAccessNotification; + BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + Deserialize((byte[])HttpContext.Current.Session[_cacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[_cacheId] = Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + HttpContext.Current.Session.Remove(_cacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (HasStateChanged) + { + Persist(); + } + } + } + + */ +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0c74a63e4b..3d58ed19bd 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,16 +158,6 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll - - - False - ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.1\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -184,23 +174,10 @@ False ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.Facebook.3.0.1\lib\net45\Microsoft.Owin.Security.Facebook.dll - - - False - ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll - - - ..\packages\Microsoft.Owin.Security.MicrosoftAccount.3.0.1\lib\net45\Microsoft.Owin.Security.MicrosoftAccount.dll - False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - - ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll - ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -253,10 +230,6 @@ - - False - ..\packages\System.IdentityModel.Tokens.Jwt.4.0.1\lib\net45\System.IdentityModel.Tokens.Jwt.dll - False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 62c48ef30a..e5d15942f1 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -22,18 +22,12 @@ - - - - - - @@ -41,6 +35,5 @@ - \ No newline at end of file From fc2b3d7fc7f659077504510e30dd0cdd7d67fa6d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 14:23:55 +1100 Subject: [PATCH 158/249] fixes merge issues --- src/Umbraco.Web.UI.Client/bower.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index ce312d669a..ed2b0d7332 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -19,11 +19,11 @@ "typeahead.js": "~0.10.5", "underscore": "~1.7.0", "rgrove-lazyload": "*", - "bootstrap-social": "~4.8.0" + "bootstrap-social": "~4.8.0", "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", - "angular-dynamic-locale": "~0.1.27" + "angular-dynamic-locale": "~0.1.27" }, "exportsOverride": { "rgrove-lazyload": { @@ -35,18 +35,21 @@ "underscore": { "": "underscore-min.{js,map}" }, + "angular-dynamic-locale": { + "": "tmhDynamicLocale.min.{js,js.map}" + }, "bootstrap-social": { "": "bootstrap-social.css" }, "font-awesome": { "css": "css/font-awesome.min.css", - "fonts" : "fonts/*" + "fonts": "fonts/*" }, "bootstrap": { "ignore": "*.ignore" }, "jquery": { - "": "jquery.min.{js,map}" + "": "jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" @@ -61,7 +64,7 @@ "blueimp-tmpl": { "ignore": "*.ignore" }, - + "blueimp-canvas-to-blob": { "ignore": "*.ignore" } From 90b562a0a1d28e98164f7da21af2c58e46120dd0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 20:17:37 +1100 Subject: [PATCH 159/249] Update the PostLogin method to write the auth ticket the way that webapi is supposed to, not sure how this was actually working before because writing cookies directly with HttpContext and then also using WebApi normally doesn't work (maybe in very specific circumstances), so now the cookie writing is done consistently and it is working, prior to this i was getting lots of issues with the xsrf tokens. Updated some user model mappings for convenience and update naming conventions for some properties of the BackOfficeIdentityUser for consistency. --- .../Models/Identity/BackOfficeIdentityUser.cs | 6 +- .../Models/Identity/IdentityModelMappings.cs | 8 +- .../Security/AuthenticationExtensions.cs | 61 +++++++++++- .../BackOfficeClaimsIdentityFactory.cs | 6 +- .../Security/BackOfficeUserStore.cs | 22 ++--- .../Editors/AuthenticationController.cs | 99 +++++++++---------- .../Models/Mapping/UserModelMapper.cs | 29 +++++- .../Security/Identity/AppBuilderExtensions.cs | 2 + src/Umbraco.Web/Security/WebSecurity.cs | 23 ++--- .../Filters/AngularAntiForgeryHelper.cs | 7 +- .../UmbracoBackOfficeLogoutAttribute.cs | 2 +- 11 files changed, 175 insertions(+), 90 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 3ba5b4259a..5060cb5912 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -11,9 +11,9 @@ namespace Umbraco.Core.Models.Identity /// Gets/sets the user's real name ///
public string Name { get; set; } - public int StartContentNode { get; set; } - public int StartMediaNode { get; set; } - public string[] AllowedApplications { get; set; } + public int StartContentId { get; set; } + public int StartMediaId { get; set; } + public string[] AllowedSections { get; set; } public string Culture { get; set; } public string UserTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 8fa2703f39..def71a8982 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -18,12 +18,12 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) - .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.Language)) + .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name)) - .ForMember(user => user.StartMediaNode, expression => expression.MapFrom(user => user.StartMediaId)) - .ForMember(user => user.StartContentNode, expression => expression.MapFrom(user => user.StartContentId)) + .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId)) + .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) - .ForMember(user => user.AllowedApplications, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); + .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); } private string GetPasswordHash(string storedPass) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index e71dd0e00c..ca597a8fef 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -9,9 +9,11 @@ using System.Security.Principal; using System.Threading; using System.Web; using System.Web.Security; +using AutoMapper; using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Security { @@ -163,8 +165,60 @@ namespace Umbraco.Core.Security Expires = DateTime.Now.AddYears(-1), Path = "/" }; + //remove the external login cookie too + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; - response.Headers.AddCookies(new[] { authCookie, prevCookie }); + response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); + } + + /// + /// This adds the forms authentication cookie for webapi since cookies are handled differently + /// + /// + /// + public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) + { + if (response == null) throw new ArgumentNullException("response"); + + //remove the external login cookie + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; + + var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); + + var ticket = new FormsAuthenticationTicket( + 4, + user.Username, + DateTime.Now, + DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), + true, + userDataString, + "/" + ); + + // Encrypt the cookie using the machine key for secure transport + var encrypted = FormsAuthentication.Encrypt(ticket); + + //add the cookie + var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) + { + //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way + Expires = DateTime.Now.AddMinutes(1440), + Path = "/", + Secure = GlobalSettings.UseSSL, + HttpOnly = true + }; + + response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); + + return ticket; } /// @@ -297,8 +351,8 @@ namespace Umbraco.Core.Security private static void Logout(this HttpContextBase http, string cookieName) { if (http == null) throw new ArgumentNullException("http"); - //clear the preview cookie too - var cookies = new[] { cookieName, Constants.Web.PreviewCookieName }; + //clear the preview cookie and external login + var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalAuthenticationType }; foreach (var c in cookies) { //remove from the request @@ -411,7 +465,6 @@ namespace Umbraco.Core.Security /// The user data. /// The login timeout mins. /// The minutes persisted. - /// The cookie path. /// Name of the cookie. /// The cookie domain. private static FormsAuthenticationTicket CreateAuthTicketAndCookie(this HttpContextBase http, diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 54b537faab..b6d19b78eb 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -22,11 +22,11 @@ namespace Umbraco.Core.Security Id = user.Id, Username = user.UserName, RealName = user.Name, - AllowedApplications = user.AllowedApplications, + AllowedApplications = user.AllowedSections, Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentNode, - StartMediaNode = user.StartMediaNode + StartContentNode = user.StartContentId, + StartMediaNode = user.StartMediaId }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index dd2041fd51..0b1c95deb9 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -414,25 +414,25 @@ namespace Umbraco.Core.Security anythingChanged = true; user.Language = identityUser.Culture; } - if (user.StartMediaId != identityUser.StartMediaNode) + if (user.StartMediaId != identityUser.StartMediaId) { anythingChanged = true; - user.StartMediaId = identityUser.StartMediaNode; + user.StartMediaId = identityUser.StartMediaId; } - if (user.StartContentId != identityUser.StartContentNode) + if (user.StartContentId != identityUser.StartContentId) { anythingChanged = true; - user.StartContentId = identityUser.StartContentNode; + user.StartContentId = identityUser.StartContentId; } - if (user.AllowedSections.ContainsAll(identityUser.AllowedApplications) == false - || identityUser.AllowedApplications.ContainsAll(user.AllowedSections) == false) + if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false + || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { anythingChanged = true; foreach (var allowedSection in user.AllowedSections) { user.RemoveAllowedSection(allowedSection); } - foreach (var allowedApplication in identityUser.AllowedApplications) + foreach (var allowedApplication in identityUser.AllowedSections) { user.AddAllowedSection(allowedApplication); } @@ -448,7 +448,7 @@ namespace Umbraco.Core.Security /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName)) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -474,7 +474,7 @@ namespace Umbraco.Core.Security /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName) == false) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -500,7 +500,7 @@ namespace Umbraco.Core.Security /// public Task> GetRolesAsync(BackOfficeIdentityUser user) { - return Task.FromResult((IList)user.AllowedApplications.ToList()); + return Task.FromResult((IList)user.AllowedSections.ToList()); } /// @@ -510,7 +510,7 @@ namespace Umbraco.Core.Security /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) { - return Task.FromResult(user.AllowedApplications.InvariantContains(roleName)); + return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index a1e12401b1..0606cec1dd 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -43,7 +43,14 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - + + private BackOfficeUserManager _userManager; + + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } + } + /// /// This is a special method that will return the current users' remaining session seconds, the reason /// it is special is because this route is ignored in the UmbracoModule so that the auth ticket doesn't get @@ -85,33 +92,6 @@ namespace Umbraco.Web.Editors } } - private void AddModelErrors(IdentityResult result, string prefix = "") - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(prefix, error); - } - } - - private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) - { - var owinContext = TryGetOwinContext().Result; - - owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - - owinContext.Authentication.SignIn( - new AuthenticationProperties() { IsPersistent = isPersistent }, - await GenerateUserIdentityAsync(user)); - } - - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } - /// /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) /// @@ -154,53 +134,45 @@ namespace Umbraco.Web.Editors } [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] + [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); } - private BackOfficeUserManager _userManager; - - protected BackOfficeUserManager UserManager - { - get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } - } - /// /// Logs a user in /// /// [SetAngularAntiForgeryTokens] - public UserDetail PostLogin(LoginModel loginModel) + public HttpResponseMessage PostLogin(LoginModel loginModel) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) { + //get the user var user = Security.GetBackOfficeUser(loginModel.Username); + var userDetail = Mapper.Map(user); - //TODO: Clean up the int cast! - var ticket = UmbracoContext.Security.PerformLogin(user); + //create a response with the userDetail object + var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //TODO: Normally we'd do something like this for identity, but we're mixing and matching legacy and new here - // so we'll keep the legacy way and move forward with this in our custom handler for now, eventually replacing - // the above legacy logic with the new stuff. - - //OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); - //OwinContext.Authentication.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, - // await user.GenerateUserIdentityAsync(UserManager)); + //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because + // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) + var ticket = response.UmbracoLoginWebApi(user); var http = this.TryGetHttpContext(); if (http.Success == false) { throw new InvalidOperationException("This method requires that an HttpContext be active"); } + //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated http.Result.AuthenticateCurrentRequest(ticket, false); + + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - var result = Mapper.Map(user); - //set their remaining seconds - result.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - return result; + return response; } //return BadRequest (400), we don't want to return a 401 because that get's intercepted @@ -222,5 +194,32 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(prefix, error); + } + } + + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) + { + var owinContext = TryGetOwinContext().Result; + + owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + + owinContext.Authentication.SignIn( + new AuthenticationProperties() { IsPersistent = isPersistent }, + await GenerateUserIdentityAsync(user)); + } + + private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 0bda50e1fc..7200f2a7c2 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -5,6 +5,9 @@ using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using umbraco; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; namespace Umbraco.Web.Models.Mapping { @@ -17,7 +20,19 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) - .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => ui.Culture(user))) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) + .ForMember( + detail => detail.EmailHash, + opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) + .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); + + config.CreateMap() + .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserTypeAlias)) + .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) + .ForMember(detail => detail.AllowedSections, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) @@ -25,6 +40,18 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); + + config.CreateMap() + .ConstructUsing((IUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' + .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) + .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] {user.UserType.Alias})) + .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))); + } private static int GetIntId(object id) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 912b19fd2e..7e70ba2958 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -84,6 +84,8 @@ namespace Umbraco.Web.Security.Identity { Provider = new CookieAuthenticationProvider { + //TODO: Need to implement IUserSecurityStampStore on BackOfficeUserStore! + //// Enables the application to validate the security stamp when the user //// logs in. This is a security feature which is used when you //// change a password or add an external login to your account. diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index b6ec3680d8..19857ddbcf 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Security; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; @@ -94,18 +95,18 @@ namespace Umbraco.Web.Security /// returns the Forms Auth ticket created which is used to log them in public virtual FormsAuthenticationTicket PerformLogin(IUser user) { - var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N")) + //clear the external cookie - we do this without owin context because we're writing cookies directly to httpcontext + // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: + //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); + + var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalAuthenticationType); + if (externalLoginCookie != null) { - Id = user.Id, - AllowedApplications = user.AllowedSections.ToArray(), - RealName = user.Name, - //currently we only have one user type! - Roles = new[] { user.UserType.Alias }, - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId, - Username = user.Username, - Culture = ui.Culture(user) - }); + externalLoginCookie.Expires = DateTime.Now.AddYears(-1); + _httpContext.Response.Cookies.Set(externalLoginCookie); + } + + var ticket = _httpContext.CreateUmbracoAuthTicket(Mapper.Map(user)); LogHelper.Info("User Id: {0} logged in", () => user.Id); diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs index 2e4b4176bb..d48cd66077 100644 --- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -1,8 +1,10 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web.Helpers; using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.WebApi.Filters { @@ -54,8 +56,9 @@ namespace Umbraco.Web.WebApi.Filters { AntiForgery.Validate(cookieToken, headerToken); } - catch + catch (Exception ex) { + LogHelper.Error(typeof(AngularAntiForgeryHelper), "Could not validate XSRF token", ex); return false; } return true; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index ecde11023b..693d45c792 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.WebApi.Filters /// A filter that is used to remove the authorization cookie for the current user when the request is successful /// /// - /// This is used so that we can log a user out in conjunction with using other filters that modify the cookies collection. + /// This is used so that we can log a user OUT in conjunction with using other filters that modify the cookies collection. /// SD: I beleive this is a bug with web api since if you modify the cookies collection on the HttpContext.Current and then /// use a filter to write the cookie headers, the filter seems to have no affect at all. /// From 4dcc4807ed69b72035e4f40a9896644bfa73d6e7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 10:57:10 +1100 Subject: [PATCH 160/249] Implements IUserSecurityStore and ensures there is a security stamp token in place, have updated the repository layer to manual update this if ASPNet Identity APIs are not used to update users. --- .../Models/Identity/BackOfficeIdentityUser.cs | 13 + src/Umbraco.Core/Models/Membership/IUser.cs | 5 + src/Umbraco.Core/Models/Membership/User.cs | 18 ++ src/Umbraco.Core/Models/Rdbms/UserDto.cs | 5 + .../Persistence/Factories/UserFactory.cs | 10 +- .../Initial/DatabaseSchemaResult.cs | 6 + .../AddUserSecurityStampColumn.cs | 22 ++ .../Repositories/UserRepository.cs | 12 +- .../Security/BackOfficeUserManager.cs | 8 +- .../Security/BackOfficeUserStore.cs | 224 ++++++++++++------ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Editors/AuthenticationController.cs | 16 +- .../Editors/BackOfficeController.cs | 15 +- .../Security/Identity/AppBuilderExtensions.cs | 21 +- 14 files changed, 258 insertions(+), 118 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 5060cb5912..31c56ba013 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -2,11 +2,24 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Security; namespace Umbraco.Core.Models.Identity { public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { + + public async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await manager.CreateIdentityAsync(this, Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } + /// /// Gets/sets the user's real name /// diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 47eb074553..f1f9c23971 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -38,5 +38,10 @@ namespace Umbraco.Core.Models.Membership /// Exposes the basic profile data /// IProfile ProfileData { get; } + + /// + /// The security stamp used by ASP.Net identity + /// + string SecurityStamp { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 3e95a94d3a..7053eaf339 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -58,6 +58,7 @@ namespace Umbraco.Core.Models.Membership private IUserType _userType; private string _name; + private string _securityStamp; private List _addedSections; private List _removedSections; private ObservableCollection _sectionCollection; @@ -76,6 +77,7 @@ namespace Umbraco.Core.Models.Membership private bool _defaultToLiveEditing; + private static readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); private static readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); private static readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); private static readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); @@ -232,6 +234,22 @@ namespace Umbraco.Core.Models.Membership get { return new UserProfile(this); } } + /// + /// The security stamp used by ASP.Net identity + /// + public string SecurityStamp + { + get { return _securityStamp; } + set + { + SetPropertyValueAndDetectChanges(o => + { + _securityStamp = value; + return _securityStamp; + }, _securityStamp, SecurityStampSelector); + } + } + /// /// Used internally to check if we need to add a section in the repository to the db /// diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 392010e56d..316a487331 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -51,6 +51,11 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] [Length(10)] public string UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string SecurityStampToken { get; set; } [ResultColumn] public List User2AppDtos { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 1f71665f50..1db30302dd 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Umbraco.Core.Models.Membership; @@ -32,7 +33,9 @@ namespace Umbraco.Core.Persistence.Factories IsLockedOut = dto.NoConsole, IsApproved = dto.Disabled == false, Email = dto.Email, - Language = dto.UserLanguage + Language = dto.UserLanguage, + //make it a GUID if it's empty + SecurityStamp = dto.SecurityStampToken.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString() : dto.SecurityStampToken }; foreach (var app in dto.User2AppDtos) @@ -61,7 +64,8 @@ namespace Umbraco.Core.Persistence.Factories UserLanguage = entity.Language, UserName = entity.Name, Type = short.Parse(entity.UserType.Id.ToString(CultureInfo.InvariantCulture)), - User2AppDtos = new List() + User2AppDtos = new List(), + SecurityStampToken = entity.SecurityStamp }; foreach (var app in entity.AllowedSections) diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index ca0cf7ebd2..c2acbd3c97 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -98,6 +98,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial return new Version(7, 0, 0); } + //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess")))) + { + return new Version(7, 2, 5); + } + return UmbracoVersion.Current; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs new file mode 100644 index 0000000000..21ead0996b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs @@ -0,0 +1,22 @@ +using System.Linq; +using Umbraco.Core.Configuration; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 10, GlobalSettings.UmbracoMigrationName)] + public class AddUserSecurityStampColumn : MigrationBase + { + public override void Up() + { + //Don't exeucte if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("securityStampToken"))) return; + + Create.Column("securityStampToken").OnTable("umbracoUser").AsString(255).Nullable(); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index d13df47b71..b26daaee7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -170,7 +170,8 @@ namespace Umbraco.Core.Persistence.Repositories {"userName", "Name"}, {"userLogin", "Username"}, {"userEmail", "Email"}, - {"userLanguage", "Language"} + {"userLanguage", "Language"}, + {"securityStampToken", "SecurityStamp"} }; //create list of properties that have changed @@ -183,6 +184,15 @@ namespace Umbraco.Core.Persistence.Repositories if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { changedCols.Add("userPassword"); + + //special case - when using ASP.Net identity the user manager will take care of updating the security stamp, however + // when not using ASP.Net identity (i.e. old membership providers), we'll need to take care of updating this manually + // so we can just detect if that property is dirty, if it's not we'll set it manually + if (dirtyEntity.IsPropertyDirty("SecurityStamp") == false) + { + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } } //only update the changed cols diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index e4c58ed6f0..b410f34107 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Security : base(store) { } - + #region What we support do not currently //NOTE: Not sure if we really want/need to ever support this @@ -42,12 +42,6 @@ namespace Umbraco.Core.Security get { return false; } } - //TODO: Support this - public override bool SupportsUserSecurityStamp - { - get { return false; } - } - //TODO: Support this public override bool SupportsUserTwoFactor { diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 0b1c95deb9..f6d8222c44 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -16,20 +16,22 @@ namespace Umbraco.Core.Security IUserPasswordStore, IUserEmailStore, IUserLoginStore, - IUserRoleStore + IUserRoleStore, + IUserSecurityStampStore + + //TODO: This would require additional columns/tables for now people will need to implement this on their own + //IUserPhoneNumberStore, + //IUserTwoFactorStore, //TODO: This will require additional columns/tables //IUserLockoutStore - //TODO: Implement this - might need to add a new column for this - // http://stackoverflow.com/questions/19487322/what-is-asp-net-identitys-iusersecuritystampstoretuser-interface - //IUserSecurityStampStore - - //TODO: To do this we need to implement IQueryable - seems pretty overkill? + //TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation //IQueryableUserStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; + private bool _disposed = false; public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { @@ -53,6 +55,7 @@ namespace Umbraco.Core.Security /// protected override void DisposeResources() { + _disposed = true; } /// @@ -62,6 +65,7 @@ namespace Umbraco.Core.Security /// public Task CreateAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var userType = _userService.GetUserTypeByAlias( @@ -108,6 +112,7 @@ namespace Umbraco.Core.Security /// public async Task UpdateAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); @@ -139,6 +144,7 @@ namespace Umbraco.Core.Security /// public Task DeleteAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); @@ -164,6 +170,7 @@ namespace Umbraco.Core.Security /// public Task FindByIdAsync(int userId) { + ThrowIfDisposed(); var user = _userService.GetUserById(userId); if (user == null) { @@ -179,6 +186,7 @@ namespace Umbraco.Core.Security /// public Task FindByNameAsync(string userName) { + ThrowIfDisposed(); var user = _userService.GetByUsername(userName); if (user == null) { @@ -197,6 +205,7 @@ namespace Umbraco.Core.Security /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); @@ -212,6 +221,7 @@ namespace Umbraco.Core.Security /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash); @@ -224,6 +234,7 @@ namespace Umbraco.Core.Security /// public Task HasPasswordAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); @@ -236,6 +247,7 @@ namespace Umbraco.Core.Security /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); @@ -251,6 +263,7 @@ namespace Umbraco.Core.Security /// public Task GetEmailAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.Email); @@ -263,6 +276,7 @@ namespace Umbraco.Core.Security /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); throw new NotImplementedException(); } @@ -273,6 +287,7 @@ namespace Umbraco.Core.Security /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { + ThrowIfDisposed(); throw new NotImplementedException(); } @@ -283,6 +298,7 @@ namespace Umbraco.Core.Security /// public Task FindByEmailAsync(string email) { + ThrowIfDisposed(); var user = _userService.GetByEmail(email); var result = user == null ? null @@ -298,6 +314,7 @@ namespace Umbraco.Core.Security /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); @@ -316,6 +333,7 @@ namespace Umbraco.Core.Security /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); @@ -335,6 +353,7 @@ namespace Umbraco.Core.Security /// public Task> GetLoginsAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult((IList) user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); @@ -345,7 +364,10 @@ namespace Umbraco.Core.Security /// /// public Task FindAsync(UserLoginInfo login) - { + { + ThrowIfDisposed(); + if (login == null) throw new ArgumentNullException("login"); + //get all logins associated with the login id var result = _externalLoginService.Find(login).ToArray(); if (result.Any()) @@ -363,6 +385,117 @@ namespace Umbraco.Core.Security return Task.FromResult(null); } + + /// + /// Adds a user to a role (section) + /// + /// + /// + public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.AddAllowedSection(roleName); + } + + return Task.FromResult(0); + } + + /// + /// Removes the role (allowed section) for the user + /// + /// + /// + public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.RemoveAllowedSection(roleName); + } + + return Task.FromResult(0); + } + + /// + /// Returns the roles for this user + /// + /// + /// + public Task> GetRolesAsync(BackOfficeIdentityUser user) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult((IList)user.AllowedSections.ToList()); + } + + /// + /// Returns true if a user is in the role + /// + /// + /// + public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + } + + /// + /// Set the security stamp for the user + /// + /// + /// + public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + user.SecurityStamp = stamp; + return Task.FromResult(0); + } + + /// + /// Get the user security stamp + /// + /// + /// + public Task GetSecurityStampAsync(BackOfficeIdentityUser user) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password + return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() + ? user.PasswordHash.ToMd5() + : user.SecurityStamp); + } + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) { if (user != null) @@ -424,6 +557,11 @@ namespace Umbraco.Core.Security anythingChanged = true; user.StartContentId = identityUser.StartContentId; } + if (user.SecurityStamp != identityUser.SecurityStamp) + { + anythingChanged = true; + user.SecurityStamp = identityUser.SecurityStamp; + } if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { @@ -441,76 +579,10 @@ namespace Umbraco.Core.Security return anythingChanged; } - /// - /// Adds a user to a role (section) - /// - /// - /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + private void ThrowIfDisposed() { - if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.AddAllowedSection(roleName); - _userService.Save(found); - } - - return Task.FromResult(0); - } - - /// - /// Removes the role (allowed section) for the user - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) - { - if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.RemoveAllowedSection(roleName); - _userService.Save(found); - } - - return Task.FromResult(0); - } - - /// - /// Returns the roles for this user - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user) - { - return Task.FromResult((IList)user.AllowedSections.ToList()); - } - - /// - /// Returns true if a user is in the role - /// - /// - /// - public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) - { - return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + if (_disposed) + throw new ObjectDisposedException(GetType().Name); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c62c869e4e..61685d8fa3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -381,6 +381,7 @@ + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 0606cec1dd..9162aa801c 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -30,6 +30,7 @@ using umbraco.providers; using Microsoft.AspNet.Identity.Owin; using Newtonsoft.Json.Linq; using Umbraco.Core.Models.Identity; +using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors { @@ -146,7 +147,7 @@ namespace Umbraco.Web.Editors ///
/// [SetAngularAntiForgeryTokens] - public HttpResponseMessage PostLogin(LoginModel loginModel) + public async Task PostLogin(LoginModel loginModel) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) { @@ -161,6 +162,10 @@ namespace Umbraco.Web.Editors // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) var ticket = response.UmbracoLoginWebApi(user); + //Identity does some of it's own checks as well so we need to use it's sign in process too... this will essentially re-create the + // ticket/cookie above but we need to create the ticket now so we can assign the Current Thread User/IPrinciple below + await SignInAsync(Mapper.Map(user), isPersistent: true); + var http = this.TryGetHttpContext(); if (http.Success == false) { @@ -211,15 +216,8 @@ namespace Umbraco.Web.Editors owinContext.Authentication.SignIn( new AuthenticationProperties() { IsPersistent = isPersistent }, - await GenerateUserIdentityAsync(user)); + await user.GenerateUserIdentityAsync(UserManager)); } - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 336e1dbfde..3091f730a7 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -493,23 +493,14 @@ namespace Umbraco.Web.Editors OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); OwinContext.Authentication.SignIn( - new AuthenticationProperties() {IsPersistent = isPersistent}, - await GenerateUserIdentityAsync(user)); - } - - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; + new AuthenticationProperties() {IsPersistent = isPersistent}, + await user.GenerateUserIdentityAsync(UserManager)); } private IAuthenticationManager AuthenticationManager { get { return OwinContext.Authentication; } - } - + } /// /// Returns the server variables regarding the application state diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 7e70ba2958..cd0bccb7d6 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; using Microsoft.Owin.Security; @@ -11,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security.Identity { @@ -84,16 +87,14 @@ namespace Umbraco.Web.Security.Identity { Provider = new CookieAuthenticationProvider { - //TODO: Need to implement IUserSecurityStampStore on BackOfficeUserStore! - - //// Enables the application to validate the security stamp when the user - //// logs in. This is a security feature which is used when you - //// change a password or add an external login to your account. - //OnValidateIdentity = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) + // Enables the application to validate the security stamp when the user + // logs in. This is a security feature which is used when you + // change a password or add an external login to your account. + OnValidateIdentity = SecurityStampValidator + .OnValidateIdentity( + TimeSpan.FromMinutes(30), + (manager, user) => user.GenerateUserIdentityAsync(manager), + identity => identity.GetUserId()) } }); From 86833aa8bf75e7d11ac7e755d4fae467092dd81f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:21:41 +1100 Subject: [PATCH 161/249] Updates the back office external cookie name to be consistently cased with the other back office cookie names --- src/Umbraco.Core/Constants-Web.cs | 1 + src/Umbraco.Core/Security/AuthenticationExtensions.cs | 6 +++--- src/Umbraco.Web/Editors/BackOfficeController.cs | 4 ++-- src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs | 4 ++-- src/Umbraco.Web/Security/WebSecurity.cs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 93f62130bd..ae80c70ecd 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -24,6 +24,7 @@ public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index ca597a8fef..9addb2e782 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -166,7 +166,7 @@ namespace Umbraco.Core.Security Path = "/" }; //remove the external login cookie too - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" @@ -185,7 +185,7 @@ namespace Umbraco.Core.Security if (response == null) throw new ArgumentNullException("response"); //remove the external login cookie - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" @@ -352,7 +352,7 @@ namespace Umbraco.Core.Security { if (http == null) throw new ArgumentNullException("http"); //clear the preview cookie and external login - var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalAuthenticationType }; + var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName }; foreach (var c in cookies) { //remove from the request diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 3091f730a7..258ca98b8d 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -479,9 +479,9 @@ namespace Umbraco.Web.Editors ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; //Remove the cookie otherwise this message will keep appearing - if (Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType] != null) + if (Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName] != null) { - Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType].Expires = DateTime.MinValue; + Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName].Expires = DateTime.MinValue; } } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index cd0bccb7d6..b8575e6875 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -111,12 +111,12 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.SetDefaultSignInAsAuthenticationType("UmbracoExternalCookie"); + app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, AuthenticationMode = AuthenticationMode.Passive, - CookieName = Constants.Security.BackOfficeExternalAuthenticationType, + CookieName = Constants.Security.BackOfficeExternalCookieName, ExpireTimeSpan = TimeSpan.FromMinutes(5), //Custom cookie manager so we can filter requests CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor()), diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 19857ddbcf..463b2c84e3 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.Security // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); - var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalAuthenticationType); + var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalCookieName); if (externalLoginCookie != null) { externalLoginCookie.Expires = DateTime.Now.AddYears(-1); From b67250c3d5d983f74bfb8ae0836f6d034db728c1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:34:43 +1100 Subject: [PATCH 162/249] Updates back office auth extension methods to be explicit with specifying that it's for back office because we need to explicitly tell each provider the SignInAsAuthenticationType so that it uses the back office auth provider and not a user's own front-end one. --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 8 +++---- ....cs => UmbracoBackOfficeAuthExtensions.cs} | 21 ++++++++++++------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 4 files changed, 21 insertions(+), 16 deletions(-) rename src/Umbraco.Web.UI/App_Code/{IdentityAuthExtensions.cs => UmbracoBackOfficeAuthExtensions.cs} (91%) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9fd1385311..6c766f8e97 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.2.5")] -[assembly: AssemblyInformationalVersion("7.2.5")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.3.0")] +[assembly: AssemblyInformationalVersion("7.3.0")] \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 6d35ec5975..c54db8d53b 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -47,10 +47,10 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureActiveDirectory("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); + //app.ConfigureBackOfficeGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); } } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs similarity index 91% rename from src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs rename to src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs index c3aaf9345d..0c3afa36ad 100644 --- a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs @@ -16,7 +16,7 @@ using Umbraco.Core; namespace Umbraco.Web.UI { - public static class IdentityAuthExtensions + public static class UmbracoBackOfficeAuthExtensions { /* @@ -42,12 +42,13 @@ namespace Umbraco.Web.UI /// http://go.microsoft.com/fwlink/?LinkID=144070 /// /// - public static void ConfigureMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) { var msOptions = new MicrosoftAccountAuthenticationOptions { ClientId = clientId, - ClientSecret = clientSecret + ClientSecret = clientSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; @@ -59,7 +60,7 @@ namespace Umbraco.Web.UI */ /* - + /// /// Configure google sign-in /// @@ -80,12 +81,13 @@ namespace Umbraco.Web.UI /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials /// /// - public static void ConfigureGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) { var googleOptions = new GoogleOAuth2AuthenticationOptions { ClientId = clientId, - ClientSecret = clientSecret + ClientSecret = clientSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; @@ -96,6 +98,7 @@ namespace Umbraco.Web.UI */ + /* /// @@ -118,12 +121,13 @@ namespace Umbraco.Web.UI /// https://developers.facebook.com/ /// /// - public static void ConfigureFacebookAuth(this IAppBuilder app, string appId, string appSecret) + public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret) { var fbOptions = new FacebookAuthenticationOptions { AppId = appId, AppSecret = appSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; @@ -167,7 +171,7 @@ namespace Umbraco.Web.UI /// This configuration requires the NaiveSessionCache class below which will need to be un-commented /// /// - public static void ConfigureActiveDirectory(this IAppBuilder app, + public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, string tenant, string clientId, string postLoginRedirectUri, string appKey, string authType) { @@ -178,6 +182,7 @@ namespace Umbraco.Web.UI var adOptions = new OpenIdConnectAuthenticationOptions { AuthenticationType = authType, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, ClientId = clientId, Authority = authority, PostLogoutRedirectUri = postLoginRedirectUri, diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 3d58ed19bd..4d5120ff2b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -356,7 +356,7 @@ Properties\SolutionInfo.cs - + loadStarterKits.ascx ASPXCodeBehind @@ -2586,7 +2586,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7301 + http://localhost:7300 False False From bf59510c68a5153ed9a60827951ee1a068853bc0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:36:07 +1100 Subject: [PATCH 163/249] Removes setting the default sign in auth type - this is a user setting, we cannot modify that. --- src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index b8575e6875..bd8e47b6b3 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -111,7 +111,6 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, From 880c9cf679796b586384da8637020f2cad56be49 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:29:34 +1100 Subject: [PATCH 164/249] Updates back office extensions to use AuthenticationDescriptionOptionsExtension to configure the options for umb back office --- .../UmbracoBackOfficeAuthExtensions.cs | 233 +++++++++--------- ...henticationDescriptionOptionsExtensions.cs | 22 ++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs diff --git a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs index 0c3afa36ad..59dea32f29 100644 --- a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs @@ -8,41 +8,45 @@ using System.Web; using Microsoft.Owin; using Owin; using Umbraco.Core; -//using Microsoft.Owin.Security.MicrosoftAccount; -//using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Umbraco.Web.Security.Identity; //using Microsoft.Owin.Security.Facebook; //using Microsoft.Owin.Security.Google; //using Microsoft.Owin.Security.OpenIdConnect; +//using Microsoft.Owin.Security.MicrosoftAccount; +//using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace Umbraco.Web.UI { public static class UmbracoBackOfficeAuthExtensions { - /* - /// - /// Configure microsoft account sign-in - /// - /// - /// - /// + /// + /// Configure microsoft account sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.MicrosoftAccount /// - /// Nuget installation: - /// Microsoft.Owin.Security.MicrosoftAccount - /// - /// Microsoft account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT - /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx - /// - /// Microsoft apps can be created here: - /// - /// http://go.microsoft.com/fwlink/?LinkID=144070 - /// - /// - public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + /// Microsoft account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT + /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx + /// + /// Microsoft apps can be created here: + /// + /// http://go.microsoft.com/fwlink/?LinkID=144070 + /// + /// + public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Microsoft", string style = "btn-microsoft", string icon = "fa-windows") { var msOptions = new MicrosoftAccountAuthenticationOptions { @@ -50,38 +54,37 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - msOptions.Description.Properties["SocialIcon"] = "fa-windows"; - msOptions.Caption = "Microsoft"; + msOptions.Description.ForUmbracoBackOffice(style, icon); + msOptions.Caption = caption; app.UseMicrosoftAccountAuthentication(msOptions); } - */ - /* - - /// - /// Configure google sign-in - /// - /// - /// - /// + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") { var googleOptions = new GoogleOAuth2AuthenticationOptions { @@ -89,39 +92,37 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; - googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; - googleOptions.Caption = "Google"; + googleOptions.Description.ForUmbracoBackOffice(style, icon); + googleOptions.Caption = caption; app.UseGoogleAuthentication(googleOptions); } - */ - - /* - - /// - /// Configure facebook sign-in - /// - /// - /// - /// + /// + /// Configure facebook sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Facebook /// - /// Nuget installation: - /// Microsoft.Owin.Security.Facebook - /// - /// Facebook account documentation for ASP.Net Identity can be found: + /// Facebook account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK + /// + /// Facebook apps can be created here: /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK - /// - /// Facebook apps can be created here: - /// - /// https://developers.facebook.com/ - /// - /// - public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret) + /// https://developers.facebook.com/ + /// + /// + public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret, + string caption = "Facebook", string style = "btn-facebook", string icon = "fa-facebook") { var fbOptions = new FacebookAuthenticationOptions { @@ -129,51 +130,49 @@ namespace Umbraco.Web.UI AppSecret = appSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; - fbOptions.Description.Properties["SocialIcon"] = "fa-facebook"; - fbOptions.Caption = "Facebook"; + fbOptions.Description.ForUmbracoBackOffice(style, icon); + fbOptions.Caption = caption; app.UseFacebookAuthentication(fbOptions); } - - */ - /* - - /// - /// Configure ActiveDirectory sign-in - /// - /// - /// - /// - /// - /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; - /// - /// - /// - /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the - /// loginProvider field in the database which looks something like this (for example): - /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ - /// and is based on your AD setup. This value needs to match in order for accounts to - /// detected as linked/un-linked in the back office. - /// + /// + /// Configure ActiveDirectory sign-in + /// + /// + /// + /// + /// + /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; + /// + /// + /// + /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the + /// loginProvider field in the database which looks something like this (for example): + /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ + /// and is based on your AD setup. This value needs to match in order for accounts to + /// detected as linked/un-linked in the back office. + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.OpenIdConnect + /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory /// - /// Nuget installation: - /// Microsoft.Owin.Security.OpenIdConnect - /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory - /// - /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet /// - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - /// - /// This configuration requires the NaiveSessionCache class below which will need to be un-commented - /// - /// + /// This configuration requires the NaiveSessionCache class below which will need to be un-commented + /// + /// public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, string tenant, string clientId, string postLoginRedirectUri, string appKey, - string authType) + string authType, + string caption = "Active Directory", string style = "btn-microsoft", string icon = "fa-windows") { const string aadInstance = "https://login.windows.net/{0}"; const string graphResourceId = "https://graph.windows.net"; @@ -209,14 +208,16 @@ namespace Umbraco.Web.UI } }; - adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - adOptions.Description.Properties["SocialIcon"] = "fa-windows"; - adOptions.Caption = "Active Directory"; + adOptions.Description.ForUmbracoBackOffice(style, icon); + adOptions.Caption = caption; app.UseOpenIdConnectAuthentication(adOptions); } - - */ + + */ + } + + /* diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs new file mode 100644 index 0000000000..07ed17f423 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Owin.Security; + +namespace Umbraco.Web.Security.Identity +{ + public static class AuthenticationDescriptionOptionsExtensions + { + /// + /// Configures the properties of the authentication description instance for use with Umbraco back office + /// + /// + /// + /// + public static void ForUmbracoBackOffice(this AuthenticationDescription options, string style, string icon) + { + options.Properties["SocialStyle"] = style; + options.Properties["SocialIcon"] = icon; + + //flag for use in back office + options.Properties["UmbracoBackOffice"] = true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0b8fea4a2f..72b3cd0262 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -551,6 +551,7 @@ + From abf70cd3020523169d889bd0739ee043e117b7d7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:35:03 +1100 Subject: [PATCH 165/249] filters external login providers in the back office to only show the ones configured for umbraco back office --- src/Umbraco.Web.UI/umbraco/Views/Default.cshtml | 2 +- src/Umbraco.Web/Editors/BackOfficeController.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index a392942fe1..b37c14b421 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -58,11 +58,11 @@ @{ var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) .Select(p => new { authType = p.AuthenticationType, caption = p.Caption, - //TODO: Need to see if this exposes any sensitive data! properties = p.Properties }) .ToArray(); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 258ca98b8d..3a6f91eb2c 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -395,6 +395,7 @@ namespace Umbraco.Web.Editors { { "providers", HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) .Select(p => new { authType = p.AuthenticationType, caption = p.Caption, From 349cb91e3a4d37f4ab9648373ca095f2ec5a9431 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:48:57 +1100 Subject: [PATCH 166/249] updates expires logic --- src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs index 448e04222c..c266b1ed5e 100644 --- a/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs +++ b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs @@ -13,7 +13,8 @@ namespace Umbraco.Web.Mvc { if (filterContext.IsChildAction) base.OnResultExecuting(filterContext); - filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); + filterContext.HttpContext.Response.Cache.SetExpires(DateTime.Now.AddDays(-10)); + filterContext.HttpContext.Response.Cache.SetLastModified(DateTime.Now); filterContext.HttpContext.Response.Cache.SetValidUntilExpires(false); filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); From 140d3c026876fa7f8806b0df25a8c5b7f05d0612 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 16:18:38 +1100 Subject: [PATCH 167/249] Update to latest Identity. Moves startup code to App_Start like VS templates have, this will be better for our nuget packages (like i do in UmbracoIdentity which works for both web apps and websites). Updates web.config to explicitly declare the owin startup otherwise we'll end up with conflicts (YSODs) and now people can configure it properly. --- src/Umbraco.Core/Umbraco.Core.csproj | 10 ++- src/Umbraco.Core/packages.config | 4 +- .../{App_Code => App_Start}/OwinStartup.cs | 6 +- .../UmbracoBackOfficeAuthExtensions.cs | 74 ++++++++++--------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 13 ++-- src/Umbraco.Web.UI/packages.config | 4 +- src/Umbraco.Web.UI/web.Template.Debug.config | 3 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 10 ++- src/Umbraco.Web/packages.config | 4 +- 10 files changed, 70 insertions(+), 60 deletions(-) rename src/Umbraco.Web.UI/{App_Code => App_Start}/OwinStartup.cs (89%) rename src/Umbraco.Web.UI/{App_Code => App_Start}/UmbracoBackOfficeAuthExtensions.cs (86%) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 61685d8fa3..523fde9136 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,11 +54,13 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll - - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + False + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll False diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 6281d1338f..fd68072884 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -3,8 +3,8 @@ - - + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Start/OwinStartup.cs similarity index 89% rename from src/Umbraco.Web.UI/App_Code/OwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/OwinStartup.cs index c54db8d53b..9279750636 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/OwinStartup.cs @@ -5,12 +5,12 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup(typeof(OwinStartup))] +[assembly: OwinStartup("UmbracoStartup", typeof(OwinStartup))] namespace Umbraco.Web.UI { /// - /// Default OWIN startup class + /// Default OWIN startup class as specified in appSettings /// public class OwinStartup { @@ -47,7 +47,7 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureBackOfficeGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeGoogleAuth("1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com", "ak0msWvSE4w9nujcsfVy8_Y0"); //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); diff --git a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs similarity index 86% rename from src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs index 59dea32f29..180334e5a6 100644 --- a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs @@ -59,44 +59,46 @@ namespace Umbraco.Web.UI app.UseMicrosoftAccountAuthentication(msOptions); } + */ - /// - /// Configure google sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - { - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - }; - googleOptions.Description.ForUmbracoBackOffice(style, icon); - googleOptions.Caption = caption; - app.UseGoogleAuthentication(googleOptions); - } + ///// + ///// Configure google sign-in + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// Nuget installation: + ///// Microsoft.Owin.Security.Google + ///// + ///// Google account documentation for ASP.Net Identity can be found: + ///// + ///// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + ///// + ///// Google apps can be created here: + ///// + ///// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + ///// + ///// + //public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + // string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") + //{ + // var googleOptions = new GoogleOAuth2AuthenticationOptions + // { + // ClientId = clientId, + // ClientSecret = clientSecret, + // SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType + // }; + // googleOptions.Description.ForUmbracoBackOffice(style, icon); + // googleOptions.Caption = caption; + // app.UseGoogleAuthentication(googleOptions); + //} + /* /// /// Configure facebook sign-in diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4d5120ff2b..bc2269b1b2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -152,10 +152,12 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + True - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll @@ -355,8 +357,8 @@ Properties\SolutionInfo.cs - - + + loadStarterKits.ascx ASPXCodeBehind @@ -2551,6 +2553,7 @@ + diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index e5d15942f1..3de27f18a4 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -9,8 +9,8 @@ - - + + diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 732e2e76ce..caa949f801 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -61,8 +61,9 @@ + - + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index b769cc7318..5357158046 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -51,7 +51,7 @@ - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 72b3cd0262..45c4b51efb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -131,11 +131,13 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + False + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index bd20574668..0257d9b37a 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,8 +6,8 @@ - - + + From e46849206436196e60c9d7b9ef79b792b060e1ba Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 19:01:29 +1100 Subject: [PATCH 168/249] Updates back office ext methods to include the CallbackPath which is key to make multi-tenanted work and ensures that the back office providers are linked with the umbraco back office external cookie provider. Adds some docs about it too. Updates the web.config templates to ensure the correct assembly redirects. --- src/Umbraco.Core/Constants-Web.cs | 9 ++ .../UmbracoBackOfficeAuthExtensions.cs | 96 ++++++++++--------- src/Umbraco.Web.UI/web.Template.Debug.config | 28 ++++++ ...henticationDescriptionOptionsExtensions.cs | 17 +++- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index ae80c70ecd..a936b2e388 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -26,6 +26,15 @@ public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs index 180334e5a6..0090029bf4 100644 --- a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs @@ -54,51 +54,52 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - msOptions.Description.ForUmbracoBackOffice(style, icon); + msOptions.ForUmbracoBackOffice(style, icon); msOptions.Caption = caption; app.UseMicrosoftAccountAuthentication(msOptions); } - */ - - ///// - ///// Configure google sign-in - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// Nuget installation: - ///// Microsoft.Owin.Security.Google - ///// - ///// Google account documentation for ASP.Net Identity can be found: - ///// - ///// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - ///// - ///// Google apps can be created here: - ///// - ///// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - ///// - ///// - //public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - // string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - //{ - // var googleOptions = new GoogleOAuth2AuthenticationOptions - // { - // ClientId = clientId, - // ClientSecret = clientSecret, - // SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - // }; - // googleOptions.Description.ForUmbracoBackOffice(style, icon); - // googleOptions.Caption = caption; - // app.UseGoogleAuthentication(googleOptions); - //} - - /* + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google + /// + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") + { + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret, + //In order to allow using different google providers on the front-end vs the back office, + // these settings are very important to make them distinguished from one another. + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, + // By default this is '/signin-google', you will need to change that default value in your + // Google developer settings for your web-app in the "REDIRECT URIS" setting + CallbackPath = new PathString("/umbraco-google-signin") + }; + googleOptions.ForUmbracoBackOffice(style, icon); + googleOptions.Caption = caption; + app.UseGoogleAuthentication(googleOptions); + } /// /// Configure facebook sign-in @@ -130,14 +131,19 @@ namespace Umbraco.Web.UI { AppId = appId, AppSecret = appSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType + //In order to allow using different google providers on the front-end vs the back office, + // these settings are very important to make them distinguished from one another. + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, + // By default this is '/signin-facebook', you will need to change that default value in your + // Facebook developer settings for your app in the Advanced settings under "Client OAuth Login" + // in the "Valid OAuth redirect URIs", specify the full URL, for example: http://mysite.com/umbraco-facebook-signin + CallbackPath = new PathString("/umbraco-facebook-signin") }; - fbOptions.Description.ForUmbracoBackOffice(style, icon); + fbOptions.ForUmbracoBackOffice(style, icon); fbOptions.Caption = caption; app.UseFacebookAuthentication(fbOptions); } - /// /// Configure ActiveDirectory sign-in /// @@ -210,7 +216,7 @@ namespace Umbraco.Web.UI } }; - adOptions.Description.ForUmbracoBackOffice(style, icon); + adOptions.ForUmbracoBackOffice(style, icon); adOptions.Caption = caption; app.UseOpenIdConnectAuthentication(adOptions); } diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index caa949f801..476d664dc5 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -172,6 +172,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs index 07ed17f423..47ad1b4310 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Owin.Security; +using Umbraco.Core; namespace Umbraco.Web.Security.Identity { @@ -10,13 +11,21 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void ForUmbracoBackOffice(this AuthenticationDescription options, string style, string icon) + public static void ForUmbracoBackOffice(this AuthenticationOptions options, string style, string icon) { - options.Properties["SocialStyle"] = style; - options.Properties["SocialIcon"] = icon; + Mandate.ParameterNotNullOrEmpty(options.AuthenticationType, "options.AuthenticationType"); + + //Ensure the prefix is set + if (options.AuthenticationType.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix) == false) + { + options.AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationTypePrefix + options.AuthenticationType; + } + + options.Description.Properties["SocialStyle"] = style; + options.Description.Properties["SocialIcon"] = icon; //flag for use in back office - options.Properties["UmbracoBackOffice"] = true; + options.Description.Properties["UmbracoBackOffice"] = true; } } } \ No newline at end of file From 6efd14eff32d548915fd288f7b31ad514ec30b54 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Mar 2015 17:43:22 +1100 Subject: [PATCH 169/249] Updates the startup auth code extension methods to better support extensibility so people could override the default user store or manager in order to implement some interfaces that we currently don't. --- .../Models/Identity/BackOfficeIdentityUser.cs | 2 +- .../Models/Identity/IdentityUser.cs | 23 +++--- .../Security/BackOfficeUserManager.cs | 82 ++++++++++--------- .../Editors/BackOfficeController.cs | 6 ++ .../Security/Identity/AppBuilderExtensions.cs | 32 ++++++++ 5 files changed, 97 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 31c56ba013..1523cf9040 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Identity public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { - public async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) + public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) { // NOTE the authenticationType must match the umbraco one // defined in CookieAuthenticationOptions.AuthenticationType diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index 09306bb1f0..cba4fc514a 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -18,6 +18,18 @@ namespace Umbraco.Core.Models.Identity where TRole : IdentityUserRole where TClaim : IdentityUserClaim { + + /// + /// Constructor + /// + /// + public IdentityUser() + { + this.Claims = new List(); + this.Roles = new List(); + this.Logins = new List(); + } + /// /// Email /// @@ -108,15 +120,6 @@ namespace Umbraco.Core.Models.Identity /// public virtual string UserName { get; set; } - /// - /// Constructor - /// - /// - public IdentityUser() - { - this.Claims = new List(); - this.Roles = new List(); - this.Logins = new List(); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b410f34107..def46b7556 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -13,47 +13,14 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { /// - /// Back office user manager + /// Default back office user manager /// - public class BackOfficeUserManager : UserManager + public class BackOfficeUserManager : BackOfficeUserManager { public BackOfficeUserManager(IUserStore store) : base(store) { } - - #region What we support do not currently - - //NOTE: Not sure if we really want/need to ever support this - public override bool SupportsUserClaim - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsQueryableUsers - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserLockout - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserTwoFactor - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserPhoneNumber - { - get { return false; } - } - #endregion /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager @@ -155,10 +122,51 @@ namespace Umbraco.Core.Security return manager; } + } - protected override void Dispose(bool disposing) + /// + /// Generic Back office user manager + /// + public class BackOfficeUserManager : UserManager + where T : BackOfficeIdentityUser + { + public BackOfficeUserManager(IUserStore store) + : base(store) { - base.Dispose(disposing); } + + #region What we support do not currently + + //NOTE: Not sure if we really want/need to ever support this + public override bool SupportsUserClaim + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsQueryableUsers + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserLockout + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserTwoFactor + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserPhoneNumber + { + get { return false; } + } + #endregion + } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 3a6f91eb2c..40ca7d428a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -472,6 +472,12 @@ namespace Umbraco.Web.Editors var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) { + //TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we + // wouldn't necessarily sign the user in here with the standard login, instead we'd update the + // UseUmbracoBackOfficeExternalCookieAuthentication extension method to have the correct provider and claims factory, + // ticket format, etc.. to create our back office user including the claims assigned and in this method we'd just ensure + // that the ticket is created and stored and that the user is logged in. + //sign in await SignInAsync(user, isPersistent: false); } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index bd8e47b6b3..c1cff6a5aa 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -31,6 +32,9 @@ namespace Umbraco.Web.Security.Identity ApplicationContext appContext, MembershipProviderBase userMembershipProvider) { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); + //Don't proceed if the app is not ready if (appContext.IsConfigured == false || appContext.DatabaseContext == null @@ -57,6 +61,10 @@ namespace Umbraco.Web.Security.Identity MembershipProviderBase userMembershipProvider, BackOfficeUserStore customUserStore) { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); + if (customUserStore == null) throw new ArgumentNullException("customUserStore"); + //Don't proceed if the app is not ready if (appContext.IsConfigured == false || appContext.DatabaseContext == null @@ -70,6 +78,30 @@ namespace Umbraco.Web.Security.Identity userMembershipProvider)); } + /// + /// Configure a custom BackOfficeUserManager for Umbraco + /// + /// + /// + /// + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + Func, IOwinContext, TManager> userManager) + where TManager : BackOfficeUserManager + where TUser : BackOfficeIdentityUser + { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userManager == null) throw new ArgumentNullException("userManager"); + + //Don't proceed if the app is not ready + if (appContext.IsConfigured == false + || appContext.DatabaseContext == null + || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + //Configure Umbraco user manager to be created per request + app.CreatePerOwinContext(userManager); + } + /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// From 394cab5ab4e3288053252630e37db1680f993a28 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Mar 2015 18:09:48 +1100 Subject: [PATCH 170/249] Updates the owin startup classes - we now have a default one shipped as a DLL which will always execute based on the appSettings, then we can ship with 2 optional ones that people can learn and use from which just requires them to update the appSetting. Now to decide on how to ship these .cs files --- ...Startup.cs => CustomUmbracoOwinStartup.cs} | 18 ++++--- .../App_Start/StandardUmbracoOwinStartup.cs | 50 +++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 3 +- src/Umbraco.Web.UI/web.Template.Debug.config | 2 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/DefaultUmbracoOwinStartup.cs | 35 +++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 7 files changed, 101 insertions(+), 10 deletions(-) rename src/Umbraco.Web.UI/App_Start/{OwinStartup.cs => CustomUmbracoOwinStartup.cs} (78%) create mode 100644 src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs create mode 100644 src/Umbraco.Web/DefaultUmbracoOwinStartup.cs diff --git a/src/Umbraco.Web.UI/App_Start/OwinStartup.cs b/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs similarity index 78% rename from src/Umbraco.Web.UI/App_Start/OwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs index 9279750636..6769b760a9 100644 --- a/src/Umbraco.Web.UI/App_Start/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs @@ -5,14 +5,19 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("UmbracoStartup", typeof(OwinStartup))] +[assembly: OwinStartup("CustomUmbracoStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { /// - /// Default OWIN startup class as specified in appSettings + /// A custom way to configure OWIN for Umbraco /// - public class OwinStartup + /// + /// The startup type is specified in appSettings under owin:appStartup - change it to "CustomUmbracoStartup" to use this class + /// + /// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice + /// + public class CustomUmbracoOwinStartup { public void Configuration(IAppBuilder app) { @@ -22,14 +27,13 @@ namespace Umbraco.Web.UI ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN - // cookie configuration, this must be declared after it. + //Ensure owin is configured for Umbraco back office authentication app .UseUmbracoBackOfficeCookieAuthentication() .UseUmbracoBackOfficeExternalCookieAuthentication(); /* - * Configure external logins: + * Configure external logins for the back office: * * Depending on the authentication sources you would like to enable, you will need to install * certain Nuget packages. @@ -47,7 +51,7 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureBackOfficeGoogleAuth("1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com", "ak0msWvSE4w9nujcsfVy8_Y0"); + //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); diff --git a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs new file mode 100644 index 0000000000..a684efa2be --- /dev/null +++ b/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs @@ -0,0 +1,50 @@ +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Web.Security.Identity; +using Umbraco.Web.UI; + +[assembly: OwinStartup("StandardUmbracoStartup", typeof(StandardUmbracoOwinStartup))] + +namespace Umbraco.Web.UI +{ + /// + /// The standard way to configure OWIN for Umbraco + /// + /// + /// The startup type is specified in appSettings under owin:appStartup - change it to "StandardUmbracoStartup" to use this class + /// + public class StandardUmbracoOwinStartup : DefaultUmbracoOwinStartup + { + public override void Configuration(IAppBuilder app) + { + //ensure the default options are configured + base.Configuration(app); + + /* + * Configure external logins for the back office: + * + * Depending on the authentication sources you would like to enable, you will need to install + * certain Nuget packages. + * + * For Google auth: Install-Package Microsoft.Owin.Security.Google + * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook + * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount + * + * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can + * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services + * + * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment + * the methods that you would like to use. Each method contains documentation and links to + * documentation for reference. You can also tweak the code in those extension + * methods to suit your needs. + */ + + //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index bc2269b1b2..ee56ec0fef 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -357,7 +357,8 @@ Properties\SolutionInfo.cs - + + loadStarterKits.ascx diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 476d664dc5..58c400efb3 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -61,7 +61,7 @@ - + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 5357158046..825f2253d4 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -51,7 +51,7 @@ - + diff --git a/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs b/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs new file mode 100644 index 0000000000..16c2140a91 --- /dev/null +++ b/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs @@ -0,0 +1,35 @@ +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Web; +using Umbraco.Web.Security.Identity; + +[assembly: OwinStartup("DefaultUmbracoStartup", typeof(DefaultUmbracoOwinStartup))] + +namespace Umbraco.Web +{ + /// + /// The default way to configure OWIN for Umbraco + /// + /// + /// The startup type is specified in appSettings under owin:appStartup + /// + public class DefaultUmbracoOwinStartup + { + public virtual void Configuration(IAppBuilder app) + { + //Configure the Identity user manager for use with Umbraco Back office + // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) + app.ConfigureUserManagerForUmbracoBackOffice( + ApplicationContext.Current, + Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + + //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN + // cookie configuration, this must be declared after it. + app + .UseUmbracoBackOfficeCookieAuthentication() + .UseUmbracoBackOfficeExternalCookieAuthentication(); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 45c4b51efb..ce591d090a 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -306,6 +306,7 @@ + From 1de9dbf18cd735eab646127e0f4c74e8918a5da6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 20:13:03 +1100 Subject: [PATCH 171/249] Updates build process to include building a nuget Identity package to provide extensibility points in Umbraco. --- LICENSE.md | 9 ----- build/Build.bat | 1 + build/Build.proj | 24 ++++++++++++++ build/NuSpecs/UmbracoCms.Identity.nuspec | 24 ++++++++++++++ src/Umbraco.Web.UI/App_Start/Readme.txt | 33 +++++++++++++++++++ ...Startup.cs => UmbracoCustomOwinStartup.cs} | 2 +- ...artup.cs => UmbracoStandardOwinStartup.cs} | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 +-- .../config/ClientDependency.config | 2 +- src/Umbraco.Web.UI/web.Template.Debug.config | 2 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- ...tartup.cs => UmbracoDefaultOwinStartup.cs} | 4 +-- src/umbraco.sln | 1 + 14 files changed, 92 insertions(+), 20 deletions(-) delete mode 100644 LICENSE.md create mode 100644 build/NuSpecs/UmbracoCms.Identity.nuspec create mode 100644 src/Umbraco.Web.UI/App_Start/Readme.txt rename src/Umbraco.Web.UI/App_Start/{CustomUmbracoOwinStartup.cs => UmbracoCustomOwinStartup.cs} (97%) rename src/Umbraco.Web.UI/App_Start/{StandardUmbracoOwinStartup.cs => UmbracoStandardOwinStartup.cs} (96%) rename src/Umbraco.Web/{DefaultUmbracoOwinStartup.cs => UmbracoDefaultOwinStartup.cs} (90%) diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 149435b7cb..0000000000 --- a/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -# The MIT License (MIT) # - -Copyright (c) 2013 Umbraco - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/Build.bat b/build/Build.bat index 1b715dcc15..7a464b422c 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -56,6 +56,7 @@ REN .\_BuildOutput\WebApp\Xslt\Web.config Web.config.transform ECHO Packing the NuGet release files ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Core.nuspec -Version %version% ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.nuspec -Version %version% +..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Identity.nuspec IF ERRORLEVEL 1 GOTO :showerror diff --git a/build/Build.proj b/build/Build.proj index 5be460d09b..ab346f7438 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -88,6 +88,7 @@ $(BuildFolderAbsolutePath)WebApp\ $(BuildFolderRelativeToProjects)WebPi\ $(BuildFolderAbsolutePath)WebPi\ + $(BuildFolderAbsolutePath)IdentityTemplates\ @@ -157,6 +158,28 @@ + + + + + + + + + + + + + + + + + + @@ -266,6 +289,7 @@ + $(BUILD_RELEASE) diff --git a/build/NuSpecs/UmbracoCms.Identity.nuspec b/build/NuSpecs/UmbracoCms.Identity.nuspec new file mode 100644 index 0000000000..c635b891f7 --- /dev/null +++ b/build/NuSpecs/UmbracoCms.Identity.nuspec @@ -0,0 +1,24 @@ + + + + UmbracoCms.Identity + 1.0.0 + Umbraco Extensibility for ASP.Net Identity + Umbraco HQ + Umbraco HQ + http://opensource.org/licenses/MIT + http://umbraco.com/ + http://umbraco.com/media/357769/100px_transparent.png + false + Installs files/classes to help with ASP.Net Identity extensibility for Umbraco back office + Installs classes to help with ASP.Net Identity extensibility for Umbraco + en-US + umbraco aspnet identity + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/Readme.txt b/src/Umbraco.Web.UI/App_Start/Readme.txt new file mode 100644 index 0000000000..b8897a27ea --- /dev/null +++ b/src/Umbraco.Web.UI/App_Start/Readme.txt @@ -0,0 +1,33 @@ + + _ _ __ __ ____ _____ _____ ____ + | | | | \/ | _ \| __ \ /\ / ____/ __ \ + | | | | \ / | |_) | |__) | / \ | | | | | | + | | | | |\/| | _ <| _ / / /\ \| | | | | | + | |__| | | | | |_) | | \ \ / ____ | |___| |__| | + \____/|_| |_|____/|_| \_/_/ \_\_____\____/ + +---------------------------------------------------- + +Umbraco extensibility code has been installed for ASP.Net Identity with Umbraco back office users + +The files have been installed into your App_Start folder if you have a Web Application project +or into App_Code if you have a Website project. + +All of these files include lots of code comments, documentation & notes to assist with extending +the ASP.Net Identity implementaion for back office users in Umbraco. For all 3rd party +ASP.Net providers, their dependencies will need to be manually installed. See comments in the +following files for full details: + +* StandardUmbracoOwinStartup.cs Includes code snippets to enable 3rd party ASP.Net Identity + providers to work with the Umbraco back office. + To enable the 'StandardUmbracoOwinStartup', update the web.config + appSetting "owin:appStartup" to be: "StandardUmbracoOwinStartup" + +* UmbracoCustomOwinStartup Includes code snippets to customize the Umbraco ASP.Net + Identity implementation for back office users as well as + snippets to enable 3rd party ASP.Net Identity providers to work. + To enable the 'UmbracoCustomOwinStartup', update the web.config + appSetting "owin:appStartup" to be: "UmbracoCustomOwinStartup" + +* UmbracoBackOfficeAuthExtensions Includes extension methods snippets to enable 3rd party ASP.Net + Identity providers to work with the Umbraco back office. \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs similarity index 97% rename from src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs index 6769b760a9..a50e50fa1e 100644 --- a/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("CustomUmbracoStartup", typeof(StandardUmbracoOwinStartup))] +[assembly: OwinStartup("CustomUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { diff --git a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs similarity index 96% rename from src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs index a684efa2be..0c7a06d0f5 100644 --- a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("StandardUmbracoStartup", typeof(StandardUmbracoOwinStartup))] +[assembly: OwinStartup("StandardUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ee56ec0fef..8d2c185043 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -357,9 +357,6 @@ Properties\SolutionInfo.cs - - - loadStarterKits.ascx ASPXCodeBehind @@ -2557,6 +2554,7 @@ + diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 246bff7cac..7b53338f12 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.Identity.nuspec b/build/NuSpecs/UmbracoCms.Identity.nuspec deleted file mode 100644 index c635b891f7..0000000000 --- a/build/NuSpecs/UmbracoCms.Identity.nuspec +++ /dev/null @@ -1,24 +0,0 @@ - - - - UmbracoCms.Identity - 1.0.0 - Umbraco Extensibility for ASP.Net Identity - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Installs files/classes to help with ASP.Net Identity extensibility for Umbraco back office - Installs classes to help with ASP.Net Identity extensibility for Umbraco - en-US - umbraco aspnet identity - - - - - - - - \ No newline at end of file diff --git a/build/NuSpecs/UmbracoExamine.PDF.nuspec b/build/NuSpecs/UmbracoExamine.PDF.nuspec deleted file mode 100644 index 5d1afff2b5..0000000000 --- a/build/NuSpecs/UmbracoExamine.PDF.nuspec +++ /dev/null @@ -1,23 +0,0 @@ - - - - UmbracoExamine.PDF - 0.7.0 - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - UmbracoExmine.PDF - umbraco - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/Readme.txt b/src/Umbraco.Web.UI/App_Start/Readme.txt deleted file mode 100644 index b8897a27ea..0000000000 --- a/src/Umbraco.Web.UI/App_Start/Readme.txt +++ /dev/null @@ -1,33 +0,0 @@ - - _ _ __ __ ____ _____ _____ ____ - | | | | \/ | _ \| __ \ /\ / ____/ __ \ - | | | | \ / | |_) | |__) | / \ | | | | | | - | | | | |\/| | _ <| _ / / /\ \| | | | | | - | |__| | | | | |_) | | \ \ / ____ | |___| |__| | - \____/|_| |_|____/|_| \_/_/ \_\_____\____/ - ----------------------------------------------------- - -Umbraco extensibility code has been installed for ASP.Net Identity with Umbraco back office users - -The files have been installed into your App_Start folder if you have a Web Application project -or into App_Code if you have a Website project. - -All of these files include lots of code comments, documentation & notes to assist with extending -the ASP.Net Identity implementaion for back office users in Umbraco. For all 3rd party -ASP.Net providers, their dependencies will need to be manually installed. See comments in the -following files for full details: - -* StandardUmbracoOwinStartup.cs Includes code snippets to enable 3rd party ASP.Net Identity - providers to work with the Umbraco back office. - To enable the 'StandardUmbracoOwinStartup', update the web.config - appSetting "owin:appStartup" to be: "StandardUmbracoOwinStartup" - -* UmbracoCustomOwinStartup Includes code snippets to customize the Umbraco ASP.Net - Identity implementation for back office users as well as - snippets to enable 3rd party ASP.Net Identity providers to work. - To enable the 'UmbracoCustomOwinStartup', update the web.config - appSetting "owin:appStartup" to be: "UmbracoCustomOwinStartup" - -* UmbracoBackOfficeAuthExtensions Includes extension methods snippets to enable 3rd party ASP.Net - Identity providers to work with the Umbraco back office. \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs deleted file mode 100644 index 66ecfb1373..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using System.Web; -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Web.Security.Identity; -//using Microsoft.Owin.Security.Facebook; -//using Microsoft.Owin.Security.Google; -//using Microsoft.Owin.Security.OpenIdConnect; -//using Microsoft.Owin.Security.MicrosoftAccount; -//using Microsoft.IdentityModel.Clients.ActiveDirectory; - -namespace Umbraco.Web.UI -{ - public static class UmbracoBackOfficeAuthExtensions - { - /* - - /// - /// Configure microsoft account sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.MicrosoftAccount - /// - /// Microsoft account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT - /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx - /// - /// Microsoft apps can be created here: - /// - /// http://go.microsoft.com/fwlink/?LinkID=144070 - /// - /// - public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Microsoft", string style = "btn-microsoft", string icon = "fa-windows") - { - var msOptions = new MicrosoftAccountAuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - }; - msOptions.ForUmbracoBackOffice(style, icon); - msOptions.Caption = caption; - app.UseMicrosoftAccountAuthentication(msOptions); - } - - /// - /// Configure google sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - { - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - //In order to allow using different google providers on the front-end vs the back office, - // these settings are very important to make them distinguished from one another. - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - // By default this is '/signin-google', you will need to change that default value in your - // Google developer settings for your web-app in the "REDIRECT URIS" setting - CallbackPath = new PathString("/umbraco-google-signin") - }; - googleOptions.ForUmbracoBackOffice(style, icon); - googleOptions.Caption = caption; - app.UseGoogleAuthentication(googleOptions); - } - - /// - /// Configure facebook sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Facebook - /// - /// Facebook account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK - /// - /// Facebook apps can be created here: - /// - /// https://developers.facebook.com/ - /// - /// - public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret, - string caption = "Facebook", string style = "btn-facebook", string icon = "fa-facebook") - { - var fbOptions = new FacebookAuthenticationOptions - { - AppId = appId, - AppSecret = appSecret, - //In order to allow using different google providers on the front-end vs the back office, - // these settings are very important to make them distinguished from one another. - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - // By default this is '/signin-facebook', you will need to change that default value in your - // Facebook developer settings for your app in the Advanced settings under "Client OAuth Login" - // in the "Valid OAuth redirect URIs", specify the full URL, for example: http://mysite.com/umbraco-facebook-signin - CallbackPath = new PathString("/umbraco-facebook-signin") - }; - fbOptions.ForUmbracoBackOffice(style, icon); - fbOptions.Caption = caption; - app.UseFacebookAuthentication(fbOptions); - } - - /// - /// Configure ActiveDirectory sign-in - /// - /// - /// - /// - /// - /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; - /// - /// - /// - /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the - /// loginProvider field in the database which looks something like this (for example): - /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ - /// and is based on your AD setup. This value needs to match in order for accounts to - /// detected as linked/un-linked in the back office. - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Install-Package Microsoft.Owin.Security.OpenIdConnect - /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory - /// - /// ActiveDirectory account documentation for ASP.Net Identity can be found: - /// - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - /// - /// This configuration requires the NaiveSessionCache class below which will need to be un-commented - /// - /// - public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, - string tenant, string clientId, string postLoginRedirectUri, string appKey, - string authType, - string caption = "Active Directory", string style = "btn-microsoft", string icon = "fa-windows") - { - const string aadInstance = "https://login.windows.net/{0}"; - const string graphResourceId = "https://graph.windows.net"; - - var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - var adOptions = new OpenIdConnectAuthenticationOptions - { - AuthenticationType = authType, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - Notifications = new OpenIdConnectAuthenticationNotifications() - { - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - AuthorizationCodeReceived = (context) => - { - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - var result = authContext.AcquireTokenByAuthorizationCode( - context.Code, - //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD configuration. - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } - - } - - }; - adOptions.ForUmbracoBackOffice(style, icon); - adOptions.Caption = caption; - app.UseOpenIdConnectAuthentication(adOptions); - } - - */ - - } - - - - /* - - /// - /// A Session cache token storage which is required to initialize the AD Identity provider on startup - /// - /// - /// Based on the examples from the AD samples: - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - /// - /// There are some newer examples of different token storage including persistent storage here: - /// It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs - /// - /// The type of token storage will be dependent on your requirements but this should be fine for standard installations - /// - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _cacheId; - public NaiveSessionCache(string userId) - { - _cacheId = userId + "_TokenCache"; - - AfterAccess = AfterAccessNotification; - BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (HasStateChanged) - { - Persist(); - } - } - } - - */ -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs deleted file mode 100644 index a50e50fa1e..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Web.Security.Identity; -using Umbraco.Web.UI; - -[assembly: OwinStartup("CustomUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] - -namespace Umbraco.Web.UI -{ - /// - /// A custom way to configure OWIN for Umbraco - /// - /// - /// The startup type is specified in appSettings under owin:appStartup - change it to "CustomUmbracoStartup" to use this class - /// - /// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice - /// - public class CustomUmbracoOwinStartup - { - public void Configuration(IAppBuilder app) - { - //Configure the Identity user manager for use with Umbraco Back office - // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) - app.ConfigureUserManagerForUmbracoBackOffice( - ApplicationContext.Current, - Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - - //Ensure owin is configured for Umbraco back office authentication - app - .UseUmbracoBackOfficeCookieAuthentication() - .UseUmbracoBackOfficeExternalCookieAuthentication(); - - /* - * Configure external logins for the back office: - * - * Depending on the authentication sources you would like to enable, you will need to install - * certain Nuget packages. - * - * For Google auth: Install-Package Microsoft.Owin.Security.Google - * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook - * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount - * - * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can - * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services - * - * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment - * the methods that you would like to use. Each method contains documentation and links to - * documentation for reference. You can also tweak the code in those extension - * methods to suit your needs. - */ - - //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs deleted file mode 100644 index 0c7a06d0f5..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Web.Security.Identity; -using Umbraco.Web.UI; - -[assembly: OwinStartup("StandardUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] - -namespace Umbraco.Web.UI -{ - /// - /// The standard way to configure OWIN for Umbraco - /// - /// - /// The startup type is specified in appSettings under owin:appStartup - change it to "StandardUmbracoStartup" to use this class - /// - public class StandardUmbracoOwinStartup : DefaultUmbracoOwinStartup - { - public override void Configuration(IAppBuilder app) - { - //ensure the default options are configured - base.Configuration(app); - - /* - * Configure external logins for the back office: - * - * Depending on the authentication sources you would like to enable, you will need to install - * certain Nuget packages. - * - * For Google auth: Install-Package Microsoft.Owin.Security.Google - * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook - * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount - * - * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can - * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services - * - * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment - * the methods that you would like to use. Each method contains documentation and links to - * documentation for reference. You can also tweak the code in those extension - * methods to suit your needs. - */ - - //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 8d2c185043..095cf28afc 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2554,7 +2554,6 @@ - diff --git a/src/umbraco.sln b/src/umbraco.sln index 71c8e27411..d839dd0a01 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -31,7 +31,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec = ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec = ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec - ..\build\NuSpecs\UmbracoCms.Identity.nuspec = ..\build\NuSpecs\UmbracoCms.Identity.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec EndProjectSection EndProject From f01990061988089274a06eb49a3aeb353ab09c5e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 13:55:33 +1100 Subject: [PATCH 174/249] oops, not sure how the LICENSE got deleted, removed AD identity nuspec since that is in a diff repo now. Removes commented out code in umb module for auth - this is done by cookie middleware now. --- LICENSE.md | 9 +++++ .../NuSpecs/UmbracoCms.ActiveDirectory.nuspec | 26 ------------- src/Umbraco.Web/UmbracoModule.cs | 39 ------------------- src/umbraco.sln | 1 - 4 files changed, 9 insertions(+), 66 deletions(-) create mode 100644 LICENSE.md delete mode 100644 build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..c5560c3ce1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +# The MIT License (MIT) # + +Copyright (c) 2013 Umbraco + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec b/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec deleted file mode 100644 index e25bd9825d..0000000000 --- a/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec +++ /dev/null @@ -1,26 +0,0 @@ - - - - UmbracoCms.Identity.ActiveDirectory - 1.0.0 - Umbraco Extensibility for ASP.Net Identity Active Directory - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Installs files/classes to help with ASP.Net Identity Active Directory provider extensibility for Umbraco back office - Installs classes to help with ASP.Net Identity Active Directory extensibility for Umbraco - en-US - umbraco aspnet identity activedirectory - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 719f7135c4..772573d164 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -166,43 +166,6 @@ namespace Umbraco.Web RewriteToUmbracoHandler(httpContext, pcr); } - /// - /// Authenticates the request by reading the FormsAuthentication cookie and setting the - /// context and thread principle object - /// - /// - /// - /// - /// We will set the identity, culture, etc... for any request that is: - /// * A back office request - /// * An installer request - /// * A /base request (since these can be back office web service requests) - /// - static void AuthenticateRequest(object sender, EventArgs e) - { - //var app = (HttpApplication)sender; - //var http = new HttpContextWrapper(app.Context); - - //// do not process if client-side request - //if (http.Request.Url.IsClientSideRequest()) - // return; - - //var req = new HttpRequestWrapper(app.Request); - - //if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) - //{ - // //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works - // // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler - // // executes. If it is successful, it doesn't call next and assigns the current user/principal. - // // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. - - // var ticket = http.GetUmbracoAuthTicket(); - - // http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); - //} - - } - #endregion #region Methods @@ -590,8 +553,6 @@ namespace Umbraco.Web BeginRequest(new HttpContextWrapper(httpContext)); }; - app.AuthenticateRequest += AuthenticateRequest; - app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; diff --git a/src/umbraco.sln b/src/umbraco.sln index d839dd0a01..7c550c17ba 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -27,7 +27,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\UmbracoCms.ActiveDirectory.nuspec = ..\build\NuSpecs\UmbracoCms.ActiveDirectory.nuspec ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec = ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec = ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec From 924ee5c4135c770c3b017f3611504801c67b5992 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 14:02:14 +1100 Subject: [PATCH 175/249] fixes merge --- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5ddac208f1..5644db1c3b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -284,7 +284,6 @@ - From df2ce2c13331d3dc584c80ccdcb80b93ef09aaa0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 14:05:29 +1100 Subject: [PATCH 176/249] fix merge --- .../Persistence/Migrations/Initial/DatabaseSchemaCreation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index d3d97266a3..d30f9e4f3c 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, {42, typeof (AccessRuleDto)}, - {43, typeof(CacheInstructionDto)} + {43, typeof(CacheInstructionDto)}, {44, typeof (ExternalLoginDto)} }; #endregion From d185f93c35be425bc17f6195f9ab244a126cd82b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 14:29:35 +1100 Subject: [PATCH 177/249] Fixes unit tests --- src/Umbraco.Tests/AngularIntegration/JsInitializationTests.cs | 2 ++ src/Umbraco.Tests/Plugins/PluginManagerTests.cs | 2 +- src/Umbraco.Tests/Plugins/TypeFinderTests.cs | 4 ++-- src/Umbraco.Web/Models/Mapping/UserModelMapper.cs | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Tests/AngularIntegration/JsInitializationTests.cs b/src/Umbraco.Tests/AngularIntegration/JsInitializationTests.cs index 6251134dbc..1b3b14516d 100644 --- a/src/Umbraco.Tests/AngularIntegration/JsInitializationTests.cs +++ b/src/Umbraco.Tests/AngularIntegration/JsInitializationTests.cs @@ -31,7 +31,9 @@ namespace Umbraco.Tests.AngularIntegration UmbClientMgr.setUmbracoPath('Hello'); jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); });", result); } diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs index 782626e912..be8b6119d8 100644 --- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs +++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs @@ -268,7 +268,7 @@ namespace Umbraco.Tests.Plugins public void Resolves_Assigned_Mappers() { var foundTypes1 = _manager.ResolveAssignedMapperTypes(); - Assert.AreEqual(25, foundTypes1.Count()); + Assert.AreEqual(26, foundTypes1.Count()); } [Test] diff --git a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs index 576a21d5ac..906027086d 100644 --- a/src/Umbraco.Tests/Plugins/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Plugins/TypeFinderTests.cs @@ -79,8 +79,8 @@ namespace Umbraco.Tests.Plugins var originalTypesFound = TypeFinderOriginal.FindClassesOfType(_assemblies); Assert.AreEqual(originalTypesFound.Count(), typesFound.Count()); - Assert.AreEqual(7, typesFound.Count()); - Assert.AreEqual(7, originalTypesFound.Count()); + Assert.AreEqual(8, typesFound.Count()); + Assert.AreEqual(8, originalTypesFound.Count()); } [Test] diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 7200f2a7c2..afae0eb122 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -50,7 +50,8 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) - .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))); + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) + .ForMember(detail => detail.SessionId, opt => opt.MapFrom(user => user.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : user.SecurityStamp)); } From df6bb368766f6f72374bd986f6363b40e0cd9b6d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 16:04:19 +1100 Subject: [PATCH 178/249] moves notification logic to umbnotifications.directive instead of in main (not sure why it was there). Updates the AuthorizeUpgrade screen to be able to show YSOD or alert messages when there are server errors. Adds htmlhelper extensions to share between Default.cshtml and AuthorizeUpgrade.cshtml. Adds null check for BackOfficeUserManager. --- .../directives/umbnavigation.directive.js | 4 +- .../directives/umbnotifications.directive.js | 15 ++- .../src/common/services/dialog.service.js | 4 +- .../src/controllers/main.controller.js | 8 +- src/Umbraco.Web.UI.Client/src/less/grid.less | 9 ++ .../src/less/modals.less | 3 + .../src/views/common/dialogs/ysod.html | 2 +- .../Umbraco/Views/AuthorizeUpgrade.cshtml | 47 ++++----- .../umbraco/Views/Default.cshtml | 94 ++++-------------- .../Editors/AuthenticationController.cs | 14 ++- .../Editors/BackOfficeController.cs | 2 + .../HtmlHelperBackOfficeExtensions.cs | 95 +++++++++++++++++++ src/Umbraco.Web/HtmlHelperRenderExtensions.cs | 1 - src/Umbraco.Web/Umbraco.Web.csproj | 1 + 14 files changed, 187 insertions(+), 112 deletions(-) create mode 100644 src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js index b3ce9cbd15..dfee6c1daf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbnavigation.directive.js @@ -3,7 +3,7 @@ * @name umbraco.directives.directive:umbNavigation * @restrict E **/ -function leftColumnDirective() { +function umbNavigationDirective() { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template @@ -11,4 +11,4 @@ function leftColumnDirective() { }; } -angular.module('umbraco.directives').directive("umbNavigation", leftColumnDirective); +angular.module('umbraco.directives').directive("umbNavigation", umbNavigationDirective); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js index 607ae97f3f..365c212f42 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbnotifications.directive.js @@ -2,11 +2,22 @@ * @ngdoc directive * @name umbraco.directives.directive:umbNotifications */ -function notificationDirective() { +function notificationDirective(notificationsService) { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template - templateUrl: 'views/directives/umb-notifications.html' + templateUrl: 'views/directives/umb-notifications.html', + link: function (scope, element, attr, ctrl) { + + //subscribes to notifications in the notification service + scope.notifications = notificationsService.current; + scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { + if (newVal) { + scope.notifications = newVal; + } + }); + + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js index 5746fd022c..1dc25363b3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/dialog.service.js @@ -508,7 +508,7 @@ angular.module('umbraco.services') /** * @ngdoc method - * @name umbraco.services.dialogService#ysodDialog + * @name umbraco.services.dialogService#embedDialog * @methodOf umbraco.services.dialogService * @description * Opens a dialog to an embed dialog @@ -531,7 +531,7 @@ angular.module('umbraco.services') var newScope = $rootScope.$new(); newScope.error = ysodError; return openDialog({ - modalClass: "umb-modal wide", + modalClass: "umb-modal wide ysod", scope: newScope, //callback: options.callback, template: 'views/common/dialogs/ysod.html', diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index cc5dc314d1..5bf230a91b 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -15,13 +15,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ $scope.authenticated = null; $scope.avatar = "assets/img/application/logo.png"; $scope.touchDevice = appState.getGlobalState("touchDevice"); - //subscribes to notifications in the notification service - $scope.notifications = notificationsService.current; - $scope.$watch('notificationsService.current', function (newVal, oldVal, scope) { - if (newVal) { - $scope.notifications = newVal; - } - }); + $scope.removeNotification = function (index) { notificationsService.remove(index); diff --git a/src/Umbraco.Web.UI.Client/src/less/grid.less b/src/Umbraco.Web.UI.Client/src/less/grid.less index 22ea5d9836..74c2d17f7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/grid.less @@ -176,3 +176,12 @@ body { .emptySection #contentwrapper {left: 80px;} .emptySection #speechbubble {left: 0;} .emptySection #navigation {display: none} + + +.login-only #speechbubble { + z-index: 10000; + left: 0 !important; +} +.login-only #speechbubble ul { + padding-left:20px +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index dbfd4d7841..3a5b4abe27 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -193,3 +193,6 @@ height: 12px } +.umb-modal.ysod { + z-index: 10000; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html index d6b980fa3c..4d8a000d43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/ysod.html @@ -1,4 +1,4 @@ -
+
]]> - + - + - + - + - + This is some content
]]> - + - + - + - + - + - + - + - + This is some content
]]> - + - + - + - + - + This is some content]]> - + - + - + - + - + "; @@ -144,7 +258,8 @@ namespace Umbraco.Tests.Routing SetDomains1(); var routingContext = GetRoutingContext(inputUrl); - var url = routingContext.UmbracoContext.CleanedUmbracoUrl; //very important to use the cleaned up umbraco url + var url = routingContext.UmbracoContext.CleanedUmbracoUrl; + //very important to use the cleaned up umbraco url var pcr = new PublishedContentRequest(url, routingContext); // lookup domain @@ -152,7 +267,7 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(expectedCulture, pcr.Culture.Name); - SettingsForTests.HideTopLevelNodeFromPath = false; + SettingsForTests.HideTopLevelNodeFromPath = false; var finder = new ContentFinderByNiceUrl(); var result = finder.TryFindContent(pcr); @@ -191,7 +306,8 @@ namespace Umbraco.Tests.Routing expectedCulture = expectedCulture ?? System.Threading.Thread.CurrentThread.CurrentUICulture.Name; var routingContext = GetRoutingContext(inputUrl); - var url = routingContext.UmbracoContext.CleanedUmbracoUrl; //very important to use the cleaned up umbraco url + var url = routingContext.UmbracoContext.CleanedUmbracoUrl; + //very important to use the cleaned up umbraco url var pcr = new PublishedContentRequest(url, routingContext); // lookup domain @@ -209,5 +325,46 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(expectedCulture, pcr.Culture.Name); Assert.AreEqual(pcr.PublishedContent.Id, expectedNode); } + + #region Cases + [TestCase(10011, "http://domain1.com/", "en-US")] + [TestCase(100111, "http://domain1.com/", "en-US")] + [TestCase(10011, "http://domain1.fr/", "fr-FR")] + [TestCase(100111, "http://domain1.fr/", "fr-FR")] + [TestCase(1001121, "http://domain1.fr/", "de-DE")] + #endregion + public void GetCulture(int nodeId, string currentUrl, string expectedCulture) + { + var domainService = SetupDomainServiceMock(new[] + { + new UmbracoDomain("domain1.com/") + { + Id = 1, + Language = new Language("en-US"), + RootContent = new Content("test1", -1, new ContentType(-1)) {Id = 1001} + }, + new UmbracoDomain("domain1.fr/") + { + Id = 1, + Language = new Language("fr-FR"), + RootContent = new Content("test1", -1, new ContentType(-1)) {Id = 1001} + }, + new UmbracoDomain("*100112") + { + Id = 1, + Language = new Language("de-DE"), + RootContent = new Content("test1", -1, new ContentType(-1)) {Id = 100112} + } + }); + + var routingContext = GetRoutingContext("http://anything/"); + var umbracoContext = routingContext.UmbracoContext; + + var content = umbracoContext.ContentCache.GetById(nodeId); + Assert.IsNotNull(content); + + var culture = Web.Models.ContentExtensions.GetCulture(umbracoContext, domainService, null, null, content.Id, content.Path, new Uri(currentUrl)); + Assert.AreEqual(expectedCulture, culture.Name); + } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Routing/UrlRoutingTestBase.cs b/src/Umbraco.Tests/Routing/UrlRoutingTestBase.cs index 295bb695e5..5802e023f0 100644 --- a/src/Umbraco.Tests/Routing/UrlRoutingTestBase.cs +++ b/src/Umbraco.Tests/Routing/UrlRoutingTestBase.cs @@ -21,7 +21,7 @@ namespace Umbraco.Tests.Routing /// Sets up the mock domain service ///
/// - protected void SetupDomainServiceMock(IEnumerable allDomains) + protected IDomainService SetupDomainServiceMock(IEnumerable allDomains) { var domainService = Mock.Get(ServiceContext.DomainService); //setup mock domain service @@ -29,6 +29,7 @@ namespace Umbraco.Tests.Routing .Returns((bool incWildcards) => incWildcards ? allDomains : allDomains.Where(d => d.IsWildcard == false)); domainService.Setup(service => service.GetAssignedDomains(It.IsAny(), It.IsAny())) .Returns((int id, bool incWildcards) => allDomains.Where(d => d.RootContent.Id == id && (incWildcards || d.IsWildcard == false))); + return domainService.Object; } protected ServiceContext GetServiceContext(IUmbracoSettingsSection umbracoSettings, ILogger logger) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 43a81fb76a..0dce9281ef 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -174,6 +174,7 @@ + diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs index 97aff5e2d3..977f8eaf0b 100644 --- a/src/Umbraco.Web/Models/ContentExtensions.cs +++ b/src/Umbraco.Web/Models/ContentExtensions.cs @@ -20,7 +20,9 @@ namespace Umbraco.Web.Models public static CultureInfo GetCulture(this IContent content, Uri current = null) { return GetCulture(UmbracoContext.Current, - ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.DomainService, + ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.ContentService, content.Id, content.Path, current); } @@ -32,32 +34,64 @@ namespace Umbraco.Web.Models /// An instance. /// An implementation. /// An implementation. + /// An implementation. /// The content identifier. /// The content path. /// The request Uri. /// The culture that would be selected to render the content. - internal static CultureInfo GetCulture(UmbracoContext umbracoContext, IDomainService domainService, ILocalizationService localizationService, + internal static CultureInfo GetCulture(UmbracoContext umbracoContext, + IDomainService domainService, ILocalizationService localizationService, IContentService contentService, int contentId, string contentPath, Uri current) { - var route = umbracoContext.ContentCache.GetRouteById(contentId); // cached - var pos = route.IndexOf('/'); + var route = umbracoContext == null + ? null // for tests only + : umbracoContext.ContentCache.GetRouteById(contentId); // cached var domainHelper = new DomainHelper(domainService); + IDomain domain; - var domain = pos == 0 - ? null - : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + if (route == null) + { + // if content is not published then route is null and we have to work + // on non-published content (note: could optimize by checking routes?) + + var content = contentService.GetById(contentId); + if (content == null) + return GetDefaultCulture(localizationService); + + var hasDomain = domainHelper.NodeHasDomains(content.Id); + while (hasDomain == false && content != null) + { + content = content.Parent(); + hasDomain = content != null && domainHelper.NodeHasDomains(content.Id); + } + + domain = hasDomain ? domainHelper.DomainForNode(content.Id, current).UmbracoDomain : null; + } + else + { + // if content is published then we have a (cached) route + // from which we can figure out the domain + + var pos = route.IndexOf('/'); + domain = pos == 0 + ? null + : domainHelper.DomainForNode(int.Parse(route.Substring(0, pos)), current).UmbracoDomain; + } if (domain == null) - { - var defaultLanguage = localizationService.GetAllLanguages().FirstOrDefault(); - return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); - } + return GetDefaultCulture(localizationService); var wcDomain = DomainHelper.FindWildcardDomainInPath(domainService.GetAll(true), contentPath, domain.RootContent.Id); return wcDomain == null ? new CultureInfo(domain.Language.IsoCode) : new CultureInfo(wcDomain.Language.IsoCode); } + + private static CultureInfo GetDefaultCulture(ILocalizationService localizationService) + { + var defaultLanguage = localizationService.GetAllLanguages().FirstOrDefault(); + return defaultLanguage == null ? CultureInfo.CurrentUICulture : new CultureInfo(defaultLanguage.IsoCode); + } } } diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 7302913d34..a735df88f9 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1898,7 +1898,9 @@ namespace Umbraco.Web public static CultureInfo GetCulture(this IPublishedContent content, Uri current = null) { return Models.ContentExtensions.GetCulture(UmbracoContext.Current, - ApplicationContext.Current.Services.DomainService, ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.DomainService, + ApplicationContext.Current.Services.LocalizationService, + ApplicationContext.Current.Services.ContentService, content.Id, content.Path, current); } From afa6c35bb50372c8cb82eca95db6ebec72c02d00 Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 1 Apr 2015 23:27:20 +0200 Subject: [PATCH 186/249] Added overloads to RedirectToUmbracoPage methods to allow passing of a querystring --- .../Mvc/RedirectToUmbracoPageResult.cs | 139 ++++++++++++++++-- src/Umbraco.Web/Mvc/SurfaceController.cs | 64 ++++++++ 2 files changed, 191 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index e205966f78..2228104c2e 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Specialized; +using System.Linq; +using System.Web; using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Models; @@ -14,8 +17,10 @@ namespace Umbraco.Web.Mvc { private IPublishedContent _publishedContent; private readonly int _pageId; + private NameValueCollection _queryStringValues; private readonly UmbracoContext _umbracoContext; private string _url; + public string Url { get @@ -24,7 +29,7 @@ namespace Umbraco.Web.Mvc if (PublishedContent == null) { - throw new InvalidOperationException("Cannot redirect, no entity was found for id " + _pageId); + throw new InvalidOperationException(string.Format("Cannot redirect, no entity was found for id {0}", _pageId)); } var result = _umbracoContext.RoutingContext.UrlProvider.GetUrl(PublishedContent.Id); @@ -34,7 +39,7 @@ namespace Umbraco.Web.Mvc return _url; } - throw new InvalidOperationException("Could not route to entity with id " + _pageId + ", the NiceUrlProvider could not generate a URL"); + throw new InvalidOperationException(string.Format("Could not route to entity with id {0}, the NiceUrlProvider could not generate a URL", _pageId)); } } @@ -61,6 +66,26 @@ namespace Umbraco.Web.Mvc { } + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues) + : this(pageId, queryStringValues, UmbracoContext.Current) + { + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, string queryString) + : this(pageId, queryString, UmbracoContext.Current) + { + } + /// /// Creates a new RedirectToUmbracoResult /// @@ -70,6 +95,63 @@ namespace Umbraco.Web.Mvc { } + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues) + : this(publishedContent, queryStringValues, UmbracoContext.Current) + { + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString) + : this(publishedContent, queryString, UmbracoContext.Current) + { + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, UmbracoContext umbracoContext) + { + _pageId = pageId; + _umbracoContext = umbracoContext; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, UmbracoContext umbracoContext) + { + _pageId = pageId; + _queryStringValues = queryStringValues; + _umbracoContext = umbracoContext; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, string queryString, UmbracoContext umbracoContext) + { + _pageId = pageId; + _queryStringValues = ParseQueryString(queryString); + _umbracoContext = umbracoContext; + } + /// /// Creates a new RedirectToUmbracoResult /// @@ -82,16 +164,33 @@ namespace Umbraco.Web.Mvc _umbracoContext = umbracoContext; } - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, UmbracoContext umbracoContext) - { - _pageId = pageId; - _umbracoContext = umbracoContext; - } + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues, UmbracoContext umbracoContext) + { + _publishedContent = publishedContent; + _pageId = publishedContent.Id; + _queryStringValues = queryStringValues; + _umbracoContext = umbracoContext; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString, UmbracoContext umbracoContext) + { + _publishedContent = publishedContent; + _pageId = publishedContent.Id; + _queryStringValues = ParseQueryString(queryString); + _umbracoContext = umbracoContext; + } public override void ExecuteResult(ControllerContext context) { @@ -103,10 +202,26 @@ namespace Umbraco.Web.Mvc } var destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext); + + if (_queryStringValues != null && _queryStringValues.Count > 0) + { + destinationUrl = destinationUrl += "?" + string.Join("&", + _queryStringValues.AllKeys.Select(x => x + "=" + HttpUtility.UrlEncode(_queryStringValues[x]))); + } + context.Controller.TempData.Keep(); context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); } + private NameValueCollection ParseQueryString(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) + { + return HttpUtility.ParseQueryString(queryString); + } + + return null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index 1010db48df..080d8931c3 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -5,6 +5,7 @@ using System.Web.Routing; using Umbraco.Core.Models; using Umbraco.Core; using Umbraco.Web.Security; +using System.Collections.Specialized; namespace Umbraco.Web.Mvc { @@ -56,6 +57,28 @@ namespace Umbraco.Web.Mvc return new RedirectToUmbracoPageResult(pageId, UmbracoContext); } + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(pageId, queryStringValues, UmbracoContext); + } + + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, string queryString) + { + return new RedirectToUmbracoPageResult(pageId, queryString, UmbracoContext); + } + /// /// Redirects to the Umbraco page with the given id /// @@ -66,6 +89,28 @@ namespace Umbraco.Web.Mvc return new RedirectToUmbracoPageResult(publishedContent, UmbracoContext); } + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(publishedContent, queryStringValues, UmbracoContext); + } + + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, string queryString) + { + return new RedirectToUmbracoPageResult(publishedContent, queryString, UmbracoContext); + } + /// /// Redirects to the currently rendered Umbraco page /// @@ -75,6 +120,25 @@ namespace Umbraco.Web.Mvc return new RedirectToUmbracoPageResult(CurrentPage, UmbracoContext); } + /// + /// Redirects to the currently rendered Umbraco page and passes provided querystring + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(NameValueCollection queryStringValues) + { + return new RedirectToUmbracoPageResult(CurrentPage, queryStringValues, UmbracoContext); + } + + /// + /// Redirects to the currently rendered Umbraco page and passes provided querystring + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(string queryString) + { + return new RedirectToUmbracoPageResult(CurrentPage, queryString, UmbracoContext); + } /// /// Redirects to the currently rendered Umbraco URL /// From 437e7f0ffcbb112f7fec2a3c48feb332a3ca6e4e Mon Sep 17 00:00:00 2001 From: AndyButland Date: Wed, 1 Apr 2015 23:29:43 +0200 Subject: [PATCH 187/249] Removed unused usings from previous commit --- src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs | 2 -- src/Umbraco.Web/Mvc/SurfaceController.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index 2228104c2e..e0b73e05f3 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -5,8 +5,6 @@ using System.Web; using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Models; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index 080d8931c3..43de6867e2 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Concurrent; using System.Web.Mvc; -using System.Web.Routing; using Umbraco.Core.Models; using Umbraco.Core; using Umbraco.Web.Security; From a321d4d1b86233a7af9e97dc3a80411ec90519a6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Apr 2015 14:46:53 +1100 Subject: [PATCH 188/249] Allows the ability to use external logins to login to authorize upgrades, this means being able to add reserved paths at startup dynamically which is now built in as part of the AuthenticationOptionsExtensions for registering external logins for the back office. --- .../Umbraco/Views/AuthorizeUpgrade.cshtml | 10 ++- .../umbraco/Views/Default.cshtml | 2 +- .../Editors/BackOfficeController.cs | 83 ++++++++++++------- .../HtmlHelperBackOfficeExtensions.cs | 9 +- ...henticationDescriptionOptionsExtensions.cs | 31 ------- .../AuthenticationOptionsExtensions.cs | 67 +++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 +- src/Umbraco.Web/UmbracoModule.cs | 45 +++++++++- 8 files changed, 177 insertions(+), 72 deletions(-) delete mode 100644 src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs create mode 100644 src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs diff --git a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml index ac0b453bdb..dcad153fce 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml @@ -51,7 +51,15 @@ - @Html.BareMinimumServerVariables(Url, (string)ViewBag.UmbracoPath) + @{ + var externalLoginUrl = Url.Action("ExternalLogin", "BackOffice", new + { + area = ViewBag.UmbracoPath, + //Custom redirect URL since we don't want to just redirect to the back office since this is for authing upgrades + redirectUrl = Url.Action("AuthorizeUpgrade", "BackOffice") + }); + } + @Html.BareMinimumServerVariables(Url, externalLoginUrl) @Html.AngularExternalLoginInfoValues((IEnumerable)ViewBag.ExternalSignInError) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index a0c6bd225e..63a8688301 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -66,7 +66,7 @@ - @Html.BareMinimumServerVariables(Url, (string)ViewBag.UmbracoPath) + @Html.BareMinimumServerVariables(Url, Url.Action("ExternalLogin", "BackOffice", new { area = ViewBag.UmbracoPath })) @Html.AngularExternalLoginInfoValues((IEnumerable)ViewBag.ExternalSignInError) diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index fe60eb647e..0313dd868f 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -63,27 +63,9 @@ namespace Umbraco.Web.Editors /// public async Task Default() { - ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; - - //check if there's errors in the TempData, assign to view bag and render the view - if (TempData["ExternalSignInError"] != null) - { - ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); - } - - //First check if there's external login info, if there's not proceed as normal - var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( - Core.Constants.Security.BackOfficeExternalAuthenticationType); - - if (loginInfo == null) - { - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); - } - - //we're just logging in with an external source, not linking accounts - return await ExternalSignInAsync(loginInfo); - + return await RenderDefaultOrProcessExternalLoginAsync( + () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"), + () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml")); } /// @@ -92,11 +74,13 @@ namespace Umbraco.Web.Editors /// /// [HttpGet] - public ActionResult AuthorizeUpgrade() + public async Task AuthorizeUpgrade() { - ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; - - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml"); + return await RenderDefaultOrProcessExternalLoginAsync( + //The default view to render when there is no external login info or errors + () => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml"), + //The ActionResult to perform if external login is successful + () => Redirect("/")); } /// @@ -422,11 +406,15 @@ namespace Umbraco.Web.Editors } [HttpPost] - public ActionResult ExternalLogin(string provider) + public ActionResult ExternalLogin(string provider, string redirectUrl = null) { + if (redirectUrl == null) + { + redirectUrl = Url.Action("Default", "BackOffice"); + } + // Request a redirect to the external login provider - return new ChallengeResult(provider, - Url.Action("Default", "BackOffice")); + return new ChallengeResult(provider, redirectUrl); } [UmbracoAuthorize] @@ -466,9 +454,42 @@ namespace Umbraco.Web.Editors return RedirectToLocal(Url.Action("Default", "BackOffice")); } - private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo) + /// + /// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info, otherwise + /// process the external login info. + /// + /// + private async Task RenderDefaultOrProcessExternalLoginAsync(Func defaultResponse, Func externalSignInResponse) + { + if (defaultResponse == null) throw new ArgumentNullException("defaultResponse"); + if (externalSignInResponse == null) throw new ArgumentNullException("externalSignInResponse"); + + ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; + + //check if there's errors in the TempData, assign to view bag and render the view + if (TempData["ExternalSignInError"] != null) + { + ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; + return defaultResponse(); + } + + //First check if there's external login info, if there's not proceed as normal + var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( + Core.Constants.Security.BackOfficeExternalAuthenticationType); + + if (loginInfo == null) + { + return defaultResponse(); + } + + //we're just logging in with an external source, not linking accounts + return await ExternalSignInAsync(loginInfo, externalSignInResponse); + } + + private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo, Func response) { if (loginInfo == null) throw new ArgumentNullException("loginInfo"); + if (response == null) throw new ArgumentNullException("response"); // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); @@ -493,8 +514,8 @@ namespace Umbraco.Web.Editors Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName].Expires = DateTime.MinValue; } } - - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); + + return response(); } private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index dbb79443d9..e43cd21a0e 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -19,13 +19,16 @@ namespace Umbraco.Web /// /// /// - /// + /// + /// The post url used to sign in with external logins - this can change depending on for what service the external login is service. + /// Example: normal back office login or authenticating upgrade login + /// /// /// /// These are the bare minimal server variables that are required for the application to start without being authenticated, /// we will load the rest of the server vars after the user is authenticated. /// - public static IHtmlString BareMinimumServerVariables(this HtmlHelper html, UrlHelper uri, string umbracoPath) + public static IHtmlString BareMinimumServerVariables(this HtmlHelper html, UrlHelper uri, string externalLoginsUrl) { var str = @"