From 18258fe4878ed2e828150d92b149aaae367b7311 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 23 Nov 2018 16:16:06 +0100 Subject: [PATCH 01/33] Initial setup of public access dialog --- .../content/content.protect.controller.js | 46 +++++++++++ .../src/views/content/protect.html | 82 +++++++++++++++++++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 +- 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/content/protect.html diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js new file mode 100644 index 0000000000..fc2da0ef75 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -0,0 +1,46 @@ +(function () { + "use strict"; + + function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService) { + + var vm = this; + var id = $scope.currentNode.id; + + vm.loading = false; + vm.saveButtonState = "init"; + + vm.next = next; + vm.save = save; + vm.close = close; + + vm.type = null; + + function onInit() { + vm.loading = true; + // Get all member groups + memberGroupResource.getGroups().then(function (groups) { + vm.groups = groups; + vm.step = "type"; + vm.loading = false; + }); + } + + function next() { + vm.step = vm.type; + } + + function save() { + vm.saveButtonState = "busy"; + console.log("TODO: SAVE!") + } + + function close() { + navigationService.hideDialog(); + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Editors.Content.ProtectController", ContentProtectController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html new file mode 100644 index 0000000000..3823d2bba1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -0,0 +1,82 @@ +
+
+
+ +
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+ + + + + +

+ Choose how to restrict access to the page {{currentNode.name}} +

+ +
+ + + +
+ +
+ + + +
+
+ + + TODO: user config + + + +

You need to create a membergroup before you can use role-based authentication.

+
+ + + TODO: role config + + + + +
+
+ + +
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 7c25782442..15c8c1c7d2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1126,10 +1126,10 @@ To manage your website, simply open the Umbraco back office and start adding con Role based protection If you wish to control access to the page using role-based authentication, using Umbraco's member groups. - You need to create a membergroup before you can use role-based authentication + You need to create a membergroup before you can use role-based authentication. Error Page Used when people are logged on, but do not have access - Choose how to restrict access to this page + Choose how to restrict access to the page %0% is now protected Protection removed from %0% Login Page @@ -1139,7 +1139,7 @@ To manage your website, simply open the Umbraco back office and start adding con Pick the roles who have access to this page Set the login and password for this page Single user protection - If you just want to setup simple protection using a single login and password + If you just want to setup simple protection using a single login and password. Date: Sat, 24 Nov 2018 19:39:15 +0100 Subject: [PATCH 02/33] A lot more implementation :) --- .../src/common/resources/content.resource.js | 56 +++++ .../content/content.protect.controller.js | 128 ++++++++++- .../src/views/content/protect.html | 209 ++++++++++++------ src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 20 +- src/Umbraco.Web/Actions/ActionProtect.cs | 4 +- src/Umbraco.Web/Editors/ContentController.cs | 121 ++++++++++ .../Models/ContentEditing/PublicAccess.cs | 20 ++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 9 files changed, 479 insertions(+), 94 deletions(-) create mode 100644 src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 06ef8748a0..c056f70a92 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -940,6 +940,62 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ), "Failed to roll back content item with id " + contentId ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.contentResource#getPublicAccess + * @methodOf umbraco.resources.contentResource + * + * @description + * Returns the public access protection for a content item + * + * ##usage + *
+          * contentResource.getPublicAccess(contentId)
+          *    .then(function(publicAccess) {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object containing the public access protection + * + */ + getPublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "GetPublicAccess", { + contentId: contentId + }) + ), + "Failed to get public access for content item with id " + contentId + ); + }, + + // TODO KJAC: ngdoc this + updatePublicAccess: function (contentId, userName, password, roles, loginPageId, errorPageId) { + var publicAccess = { + contentId: contentId, + loginPageId: loginPageId, + errorPageId: errorPageId + }; + if (userName && password) { + publicAccess.userName = userName; + publicAccess.password = password; + } + else if (angular.isArray(roles) && roles.length) { + publicAccess.roles = roles; + } + else { + throw "must supply either userName/password or roles"; + } + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "PostPublicAccess", publicAccess) + ), + "Failed to update public access for content item with id " + contentId + ); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index fc2da0ef75..84d8619303 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService) { + function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService, editorService) { var vm = this; var id = $scope.currentNode.id; @@ -9,35 +9,147 @@ vm.loading = false; vm.saveButtonState = "init"; + vm.isValid = isValid; vm.next = next; vm.save = save; vm.close = close; + vm.toggle = toggle; + vm.pickLoginPage = pickLoginPage; + vm.pickErrorPage = pickErrorPage; + vm.remove = remove; + vm.removeConfirm = removeConfirm; vm.type = null; + vm.step = null; function onInit() { vm.loading = true; - // Get all member groups - memberGroupResource.getGroups().then(function (groups) { - vm.groups = groups; - vm.step = "type"; - vm.loading = false; + + // get the current public access protection + contentResource.getPublicAccess(id).then(function (publicAccess) { + // init the current settings for public access (if any) + vm.loginPage = publicAccess.loginPage; + vm.errorPage = publicAccess.errorPage; + vm.userName = publicAccess.userName; + vm.roles = publicAccess.roles; + vm.canRemove = true; + + if (vm.userName) { + vm.type = "user"; + next(); + } + else if (vm.roles) { + vm.type = "role"; + next(); + } + else { + vm.canRemove = false; + vm.loading = false; + } }); } function next() { - vm.step = vm.type; + if (vm.type === "role") { + vm.loading = true; + // Get all member groups + memberGroupResource.getGroups().then(function (groups) { + vm.step = vm.type; + vm.groups = groups; + // set groups "selected" according to currently selected roles for public access + _.each(groups, function(group) { + group.selected = _.contains(vm.roles, group.name); + }); + vm.loading = false; + }); + } + else { + vm.step = vm.type; + } + } + + function isValid() { + if (!vm.type) { + return false; + } + if (!vm.protectForm.$valid) { + return false; + } + if (!vm.loginPage || !vm.errorPage) { + return false; + } + if (vm.type === "role") { + return !!_.find(vm.groups, function(group) { return group.selected; }); + } + return true; } function save() { vm.saveButtonState = "busy"; - console.log("TODO: SAVE!") + var selectedGroups = _.filter(vm.groups, function(group) { return group.selected; }); + var roles = _.map(selectedGroups, function(group) { return group.name; }); + contentResource.updatePublicAccess(id, vm.userName, vm.password, roles, vm.loginPage.id, vm.errorPage.id).then( + function () { + vm.saveButtonState = "success"; + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.saveButtonState = "error"; + } + ); } function close() { navigationService.hideDialog(); } + function toggle(group) { + group.selected = !group.selected; + } + + function pickLoginPage() { + // TODO KJAC: temporary test values until we fix the content picker + if (!vm.loginPage) { + vm.loginPage = { id: 1092 }; + } + + //editorService.contentPicker({ + // submit: function(model) { + // console.log("I picked", model) + // editorService.close(); + // }, + // close: function () { + // editorService.close(); + // } + //}); + } + + function pickErrorPage() { + // TODO KJAC: temporary test values until we fix the content picker + if (!vm.errorPage) { + vm.errorPage = { id: 1093 }; + } + + //editorService.contentPicker({ + // submit: function(model) { + // console.log("I picked", model) + // editorService.close(); + // }, + // close: function () { + // editorService.close(); + // } + //}); + } + + function remove() { + vm.removing = true; + } + + function removeConfirm() { + vm.saveButtonState = "busy"; + // TODO KJAC: remove protection from the page + } + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 3823d2bba1..93381303d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -1,82 +1,151 @@
-
-
+
+
+ -
-
-
{{error.errorMsg}}
-
{{error.data.message}}
-
-
- - - - - -

- Choose how to restrict access to the page {{currentNode.name}} -

- -
- - - +
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
-
- + + - +
+

+ Choose how to restrict access to this page +

+ +
+ + + +
+ +
+ + + +
+ +
+
+

Set the login and password for this page

+ + + + + + + +
+ +
+

You need to create a membergroup before you can use role-based authentication

+
+ +
+

Pick the roles who have access to this page

+ + + + +
+ +
+

Select the pages that contain login form and error messages

+ + + Add + + + + + + + Add + + + + +
+
+ +
+

Are you sure you want to remove the protection from this page?

+
+ + + - - - TODO: user config - - - -

You need to create a membergroup before you can use role-based authentication.

-
- - - TODO: role config - - - -
-
- + + +
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 7864d3baae..9f6ca212d1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -872,20 +872,22 @@ Mange hilsner fra Umbraco robotten Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) + Rollebaseret beskyttelse Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse Fejlside Brugt når folk er logget ind, men ingen adgang - Vælg hvordan siden skal beskyttes - %0% er nu beskyttet - Beskyttelse fjernet fra %0% + %0% skal beskyttes]]> + Log ind-side Vælg siden der indeholder log ind-formularen - Fjern beskyttelse + Fjern beskyttelse... + %0%?]]> Vælg siderne der indeholder log ind-formularer og fejlmeddelelser - Vælg de roller der har adgang til denne side - Indstil login og kodeord for denne side + %0%]]> + %0%]]> Enkel brugerbeskyttelse Hvis du blot ønsker at opsætte simpel beskyttelse ved hjælp af et enkelt login og kodeord diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 15c8c1c7d2..dff3d21ea4 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1124,22 +1124,24 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) + Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups. - You need to create a membergroup before you can use role-based authentication. + If you wish to control access to the page using role-based authentication, using Umbraco's member groups + You need to create a membergroup before you can use role-based authentication Error Page Used when people are logged on, but do not have access - Choose how to restrict access to the page - %0% is now protected - Protection removed from %0% + %0%]]> + Login Page Choose the page that contains the login form - Remove Protection + Remove protection... + %0%?]]> Select the pages that contain login form and error messages - Pick the roles who have access to this page - Set the login and password for this page + %0%]]> + %0%]]> Single user protection - If you just want to setup simple protection using a single login and password. + If you just want to setup simple protection using a single login and password public class ActionProtect : IAction { - public char Letter => 'P'; + public const char ActionLetter = 'P'; + + public char Letter => ActionLetter; public string Alias => "protect"; public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; public string Icon => "lock"; diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 53ecddd015..55bbe0be18 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2153,5 +2153,126 @@ namespace Umbraco.Web.Editors return Request.CreateValidationErrorResponse(notificationModel); } + + [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [HttpGet] + public HttpResponseMessage GetPublicAccess(int contentId) + { + var content = Services.ContentService.GetById(contentId); + if (content == null) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + } + + var entry = Services.PublicAccessService.GetEntryForContent(content); + if (entry == null) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + var loginPageEntity = Services.EntityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); + var errorPageEntity = Services.EntityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); + + // unwrap the current public access setup for the client + // - this API method is the single point of entry for both "modes" of public access (single user and role based) + var userName = entry.Rules + .FirstOrDefault(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) + ?.RuleValue; + var roles = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) + .Select(rule => rule.RuleValue) + .ToArray(); + + return Request.CreateResponse(HttpStatusCode.OK, new PublicAccess + { + UserName = userName, + Roles = roles, + LoginPage = loginPageEntity != null ? Mapper.Map(loginPageEntity) : null, + ErrorPage = errorPageEntity != null ? Mapper.Map(errorPageEntity) : null + }); + } + + // set up public access using role based access + [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [HttpPost] + public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, int loginPageId, int errorPageId) + { + if (roles == null || roles.Any() == false) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + } + + var content = Services.ContentService.GetById(contentId); + var loginPage = Services.ContentService.GetById(loginPageId); + var errorPage = Services.ContentService.GetById(errorPageId); + if (content == null || loginPage == null || errorPage == null) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + } + + var entry = Services.PublicAccessService.GetEntryForContent(content); + + if (entry == null) + { + entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); + foreach (var role in roles) + { + entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); + } + } + else + { + entry.LoginNodeId = loginPage.Id; + entry.NoAccessNodeId = errorPage.Id; + + var currentRules = entry.Rules.ToArray(); + var obsoleteRules = currentRules.Where(rule => + rule.RuleType != Constants.Conventions.PublicAccess.MemberRoleRuleType + || roles.Contains(rule.RuleValue) == false + ); + var newRoles = roles.Where(group => + currentRules.Any(rule => + rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType + && rule.RuleValue == group + ) == false + ); + foreach (var rule in obsoleteRules) + { + entry.RemoveRule(rule); + } + foreach (var role in newRoles) + { + entry.AddRule(Constants.Conventions.PublicAccess.MemberRoleRuleType, role); + } + } + + Services.PublicAccessService.Save(entry); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + // set up public access using username and password + [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] + [HttpPost] + public HttpResponseMessage PostPublicAccess(int contentId, string userName, string password, int loginPageId, int errorPageId) + { + // TODO KJAC: validate password regex + if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + } + + var content = Services.ContentService.GetById(contentId); + var loginPage = Services.ContentService.GetById(loginPageId); + var errorPage = Services.ContentService.GetById(errorPageId); + if (content == null || loginPage == null || errorPage == null) + { + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + } + + // TODO KJAC: implement + + return Request.CreateResponse(HttpStatusCode.OK); + } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs new file mode 100644 index 0000000000..68236d5934 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "publicAccess", Namespace = "")] + public class PublicAccess + { + [DataMember(Name = "userName")] + public string UserName { get; set; } + + [DataMember(Name = "roles")] + public string[] Roles { get; set; } + + [DataMember(Name = "loginPage")] + public EntityBasic LoginPage { get; set; } + + [DataMember(Name = "errorPage")] + public EntityBasic ErrorPage { get; set; } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 32134e7a45..ef70df6c7d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -150,6 +150,7 @@ + From ed56845bc1d4e6891cb3372eb5a9fc5323c1b30e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 25 Nov 2018 09:17:04 +0100 Subject: [PATCH 03/33] Implement remove protection + add success messages --- .../src/common/resources/content.resource.js | 32 ++++++++++++++++- .../content/content.protect.controller.js | 22 ++++++++++-- .../src/views/content/protect.html | 36 ++++++++++++------- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 4 +-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 +-- src/Umbraco.Web/Editors/ContentController.cs | 27 ++++++++++++-- 6 files changed, 101 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index c056f70a92..a663d8e52c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -996,8 +996,38 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ), "Failed to update public access for content item with id " + contentId ); - } + }, + /** + * @ngdoc method + * @name umbraco.resources.contentResource#removePublicAccess + * @methodOf umbraco.resources.contentResource + * + * @description + * Removes the public access protection for a content item + * + * ##usage + *
+          * contentResource.removePublicAccess(contentId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @returns {Promise} resourcePromise object that's resolved once the public access has been removed + * + */ + removePublicAccess: function (contentId) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl("contentApiBaseUrl", "RemovePublicAccess", { + contentId: contentId + }) + ), + "Failed to remove public access for content item with id " + contentId + ); + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 84d8619303..3ef930d7af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService, editorService) { + function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService, localizationService) { var vm = this; var id = $scope.currentNode.id; @@ -90,7 +90,11 @@ var roles = _.map(selectedGroups, function(group) { return group.name; }); contentResource.updatePublicAccess(id, vm.userName, vm.password, roles, vm.loginPage.id, vm.errorPage.id).then( function () { - vm.saveButtonState = "success"; + localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { + vm.success = { + message: value + }; + }); navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); }, function (error) { vm.error = error; @@ -147,7 +151,19 @@ function removeConfirm() { vm.saveButtonState = "busy"; - // TODO KJAC: remove protection from the page + contentResource.removePublicAccess(id).then( + function () { + localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { + vm.success = { + message: value + }; + }); + navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); + }, function (error) { + vm.error = error; + vm.saveButtonState = "error"; + } + ); } onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 93381303d0..8e1a8259cd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -1,12 +1,12 @@
- + -
+
-
{{error.errorMsg}}
-
{{error.data.message}}
+
{{vm.error.errorMsg}}
+
{{vm.error.data.message}}
@@ -97,16 +97,26 @@ + + +
+ + +
- -
-

Select the pages that contain login form and error messages

+

Select the pages that contain login form and error messages

Add From 4192a11de092987941aa02aa6e62f321e36cfae6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 25 Nov 2018 11:02:33 +0100 Subject: [PATCH 07/33] Update ngdoc and en-us translations --- .../src/common/resources/content.resource.js | 26 ++++++++++++++++++- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 - src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 - .../Umbraco/config/lang/en_us.xml | 15 ++++++----- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index a663d8e52c..b7ea16ea19 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -973,7 +973,31 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { ); }, - // TODO KJAC: ngdoc this + /** + * @ngdoc method + * @name umbraco.resources.contentResource#updatePublicAccess + * @methodOf umbraco.resources.contentResource + * + * @description + * Sets or updates the public access protection for a content item + * + * ##usage + *
+          * contentResource.updatePublicAccess(contentId, userName, password, roles, loginPageId, errorPageId)
+          *    .then(function() {
+          *        // do your thing
+          *    });
+          * 
+ * + * @param {Int} contentId The content Id + * @param {String} userName The name of the user that should have access (if using specific user protection) + * @param {String} password The password for the user that should have access (if using specific user protection) + * @param {Array} roles The roles that should have access (if using role based protection) + * @param {Int} loginPageId The Id of the login page + * @param {Int} errorPageId The Id of the error page + * @returns {Promise} resourcePromise object containing the public access protection + * + */ updatePublicAccess: function (contentId, userName, password, roles, loginPageId, errorPageId) { var publicAccess = { contentId: contentId, diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 40718b508c..f30b4aa7bc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -872,7 +872,6 @@ Mange hilsner fra Umbraco robotten Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) - Rollebaseret beskyttelse Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 662eb705e1..0f3926adf2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1124,7 +1124,6 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) - Role based protection If you wish to control access to the page using role-based authentication, using Umbraco's member groups You need to create a membergroup before you can use role-based authentication diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 5b39f3d25f..ba4156bf93 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1146,19 +1146,20 @@ To manage your website, simply open the Umbraco back office and start adding con Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups. + If you wish to control access to the page using role-based authentication, using Umbraco's member groups You need to create a membergroup before you can use role-based authentication Error Page Used when people are logged on, but do not have access - Choose how to restrict access to this page - %0% is now protected - Protection removed from %0% + %0%]]> + %0% is now protected]]> + %0%]]> Login Page Choose the page that contains the login form - Remove Protection + Remove protection... + %0%?]]> Select the pages that contain login form and error messages - Pick the roles who have access to this page - Set the login and password for this page + %0%]]> + %0%]]> Single user protection If you just want to setup simple protection using a single login and password From 924419f403239f33c0f20051524ea92a30ef8f86 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 25 Nov 2018 11:04:28 +0100 Subject: [PATCH 08/33] Ensure that the dialog always closes on close() --- .../src/views/content/content.protect.controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index e8a6a54622..69c73b5ec9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -104,6 +104,8 @@ } function close() { + // ensure that we haven't set a locked state on the dialog before closing it + navigationService.allowHideDialog(true); navigationService.hideDialog(); } From 34416d9c0a72f759a4f914f0f0e0c9eafeb5be06 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 27 Nov 2018 22:11:18 +0100 Subject: [PATCH 09/33] Change role toggles to checkboxes and restyle the page pickers --- .../src/views/content/protect.html | 67 +++++++++++-------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 4f4c15a2d6..a405a8e569 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -55,38 +55,49 @@

Pick the roles who have access to this page

- - - - +
+ +

Select the pages that contain login form and error messages

- -
- Add - - - - - - - Add - - - - +
+
+ + + Add + + + +
+
+
+
+ + + Add + + + +
+
From 9ac3b98acf54ba70aee9f4eb69e3936aad597d86 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 28 Nov 2018 07:39:48 +0100 Subject: [PATCH 10/33] Add some indents around the various sections of configuration --- .../src/views/content/protect.html | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index a405a8e569..3746e46aa3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -18,35 +18,39 @@ Choose how to restrict access to this page

-
- + +
+ - -
+ +
-
- +
+ - -
+ +
+

Set the login and password for this page

- - - - - - - + + + + + + + + +
@@ -55,49 +59,53 @@

Pick the roles who have access to this page

-
- -
+ +
+ +
+

Select the pages that contain login form and error messages

-
-
- - - Add - - - + +
+
+ + + Add + + + +
-
-
-
- - - Add - - - +
+
+ + + Add + + + +
-
+
From 8990a8fcb05c505adf3b26a8156bb6361d8eaa75 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 1 Dec 2018 09:26:55 +0100 Subject: [PATCH 11/33] Remove the Umbraco core jQuery dependency for macro partials --- .../PartialViewMacros/Templates/EditProfile.cshtml | 6 +++--- .../Umbraco/PartialViewMacros/Templates/Login.cshtml | 6 +++--- .../PartialViewMacros/Templates/LoginStatus.cshtml | 11 ----------- .../PartialViewMacros/Templates/RegisterMember.cshtml | 6 +++--- 4 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml index d74b090c07..79c83a883d 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml @@ -9,9 +9,9 @@ Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("/umbraco/lib/jquery/jquery.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validate/jquery.validate.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"); var success = TempData["ProfileUpdateSuccess"] != null; } diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml index 7c5ed032f7..31a3bd604d 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Login.cshtml @@ -10,9 +10,9 @@ Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("/umbraco/lib/jquery/jquery.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validate/jquery.validate.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"); } @* NOTE: This RenderJsHere code should be put on your main template page where the rest of your script tags are placed *@ diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/LoginStatus.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/LoginStatus.cshtml index 4fa4bff173..8eadbb342f 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/LoginStatus.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/LoginStatus.cshtml @@ -1,5 +1,4 @@ @using System.Web.Mvc.Html -@using ClientDependency.Core.Mvc @using Umbraco.Web @using Umbraco.Web.Models @using Umbraco.Web.Controllers @@ -7,13 +6,6 @@ @{ var loginStatusModel = Members.GetCurrentLoginStatus(); - - Html.EnableClientValidation(); - Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("/umbraco/lib/jquery/jquery.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validate/jquery.validate.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"); - var logoutModel = new PostRedirectModel(); @* @@ -24,9 +16,6 @@ *@ } -@* NOTE: This RenderJsHere code should be put on your main template page where the rest of your script tags are placed *@ -@Html.RenderJsHere() - @if (loginStatusModel.IsLoggedIn) {

You are currently logged in as @loginStatusModel.Name

diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index b448a992d9..b9fbea4733 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -33,9 +33,9 @@ Html.EnableClientValidation(); Html.EnableUnobtrusiveJavaScript(); - Html.RequiresJs("/umbraco/lib/jquery/jquery.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validate/jquery.validate.min.js"); - Html.RequiresJs("/umbraco/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"); + Html.RequiresJs("https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"); var success = TempData["FormSuccess"] != null; } From a795ed66e8e14eb0d6028c35b8698fc50bfa078e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 8 Dec 2018 14:50:05 +0100 Subject: [PATCH 12/33] Use a member group picker instead of checkboxes --- .../content/content.protect.controller.js | 62 ++++++++++++++----- .../src/views/content/protect.html | 30 +++++---- src/Umbraco.Web/Editors/ContentController.cs | 2 +- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 69c73b5ec9..4504ebd372 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -7,7 +7,7 @@ var id = $scope.currentNode.id; vm.loading = false; - vm.saveButtonState = "init"; + vm.buttonState = "init"; vm.isValid = isValid; vm.next = next; @@ -16,8 +16,10 @@ vm.toggle = toggle; vm.pickLoginPage = pickLoginPage; vm.pickErrorPage = pickErrorPage; - vm.remove = remove; - vm.removeConfirm = removeConfirm; + vm.pickGroup = pickGroup; + vm.removeGroup = removeGroup; + vm.removeProtection = removeProtection; + vm.removeProtectionConfirm = removeProtectionConfirm; vm.type = null; vm.step = null; @@ -55,10 +57,10 @@ // Get all member groups memberGroupResource.getGroups().then(function (groups) { vm.step = vm.type; - vm.groups = groups; - // set groups "selected" according to currently selected roles for public access - _.each(groups, function(group) { - group.selected = _.contains(vm.roles, group.name); + vm.allGroups = groups; + vm.hasGroups = groups.length > 0; + vm.groups = _.filter(groups, function(group) { + return _.contains(vm.roles, group.name); }); vm.loading = false; }); @@ -79,15 +81,14 @@ return false; } if (vm.type === "role") { - return !!_.find(vm.groups, function(group) { return group.selected; }); + return vm.groups && vm.groups.length > 0; } return true; } function save() { - vm.saveButtonState = "busy"; - var selectedGroups = _.filter(vm.groups, function(group) { return group.selected; }); - var roles = _.map(selectedGroups, function(group) { return group.name; }); + vm.buttonState = "busy"; + var roles = _.map(vm.groups, function (group) { return group.name; }); contentResource.updatePublicAccess(id, vm.userName, vm.password, roles, vm.loginPage.id, vm.errorPage.id).then( function () { localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { @@ -98,7 +99,7 @@ navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); }, function (error) { vm.error = error; - vm.saveButtonState = "error"; + vm.buttonState = "error"; } ); } @@ -113,6 +114,35 @@ group.selected = !group.selected; } + function pickGroup() { + navigationService.allowHideDialog(false); + editorService.memberGroupPicker({ + multiPicker: true, + submit: function(model) { + var selectedGroupIds = model.selectedMemberGroups + ? model.selectedMemberGroups + : [model.selectedMemberGroup]; + _.each(selectedGroupIds, + function(groupId) { + var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); + if (group && !_.contains(vm.groups, group)) { + vm.groups.push(group); + } + }); + editorService.close(); + navigationService.allowHideDialog(true); + }, + close: function() { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeGroup(group) { + vm.groups = _.without(vm.groups, group); + } + function pickLoginPage() { pickPage(vm.loginPage); } @@ -141,12 +171,12 @@ }); } - function remove() { + function removeProtection() { vm.removing = true; } - function removeConfirm() { - vm.saveButtonState = "busy"; + function removeProtectionConfirm() { + vm.buttonState = "busy"; contentResource.removePublicAccess(id).then( function () { localizationService.localize("publicAccess_paIsRemoved", [$scope.currentNode.name]).then(function(value) { @@ -157,7 +187,7 @@ navigationService.syncTree({ tree: "content", path: $scope.currentNode.path, forceReload: true }); }, function (error) { vm.error = error; - vm.saveButtonState = "error"; + vm.buttonState = "error"; } ); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 3746e46aa3..4b2bf54b5c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -53,22 +53,26 @@
-
+

You need to create a membergroup before you can use role-based authentication

-
+

Pick the roles who have access to this page

-
- -
+ + + + Add +
-
+

Select the pages that contain login form and error messages

@@ -145,16 +149,16 @@ + disabled="vm.buttonState === 'busy'"> @@ -169,8 +173,8 @@ diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index a026977fc1..b7a33c3ab8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2240,7 +2240,7 @@ namespace Umbraco.Web.Editors } foreach (var role in newRoles) { - entry.AddRule(Constants.Conventions.PublicAccess.MemberRoleRuleType, role); + entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); } } From c860a6b574123beb0a94b6c300c7cd07b2837773 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 9 Dec 2018 14:58:52 +0100 Subject: [PATCH 13/33] Pick members instead of entering username and password --- .../src/common/resources/content.resource.js | 14 ++-- src/Umbraco.Web.UI.Client/src/less/hacks.less | 1 - .../content/content.protect.controller.js | 53 +++++++++++++-- .../src/views/content/protect.html | 30 +++++---- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 8 +-- .../Umbraco/config/lang/en_us.xml | 8 +-- src/Umbraco.Web/Editors/ContentController.cs | 64 ++++++++----------- .../Models/ContentEditing/PublicAccess.cs | 7 +- 9 files changed, 110 insertions(+), 81 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index b7ea16ea19..681a629ed0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -990,27 +990,25 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} contentId The content Id - * @param {String} userName The name of the user that should have access (if using specific user protection) - * @param {String} password The password for the user that should have access (if using specific user protection) * @param {Array} roles The roles that should have access (if using role based protection) + * @param {Array} memberIds The IDs of the members that should have access (if using member based protection) * @param {Int} loginPageId The Id of the login page * @param {Int} errorPageId The Id of the error page * @returns {Promise} resourcePromise object containing the public access protection * */ - updatePublicAccess: function (contentId, userName, password, roles, loginPageId, errorPageId) { + updatePublicAccess: function (contentId, roles, memberIds, loginPageId, errorPageId) { var publicAccess = { contentId: contentId, loginPageId: loginPageId, errorPageId: errorPageId }; - if (userName && password) { - publicAccess.userName = userName; - publicAccess.password = password; - } - else if (angular.isArray(roles) && roles.length) { + if (angular.isArray(roles) && roles.length) { publicAccess.roles = roles; } + else if (angular.isArray(memberIds) && memberIds.length) { + publicAccess.memberIds = memberIds; + } else { throw "must supply either userName/password or roles"; } diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index cd32c64782..0bbb89a250 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -106,7 +106,6 @@ iframe, .content-column-body { display: flex; flex-wrap: nowrap; flex-direction: row; - justify-content: center; align-items: flex-start; margin-top: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 4504ebd372..ace37d4ac5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -18,6 +18,8 @@ vm.pickErrorPage = pickErrorPage; vm.pickGroup = pickGroup; vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; vm.removeProtection = removeProtection; vm.removeProtectionConfirm = removeProtectionConfirm; @@ -29,24 +31,25 @@ // get the current public access protection contentResource.getPublicAccess(id).then(function (publicAccess) { + vm.loading = false; + // init the current settings for public access (if any) vm.loginPage = publicAccess.loginPage; vm.errorPage = publicAccess.errorPage; - vm.userName = publicAccess.userName; - vm.roles = publicAccess.roles; + vm.roles = publicAccess.roles || []; + vm.members = publicAccess.members || []; vm.canRemove = true; - if (vm.userName) { - vm.type = "user"; + if (vm.members.length) { + vm.type = "member"; next(); } - else if (vm.roles) { + else if (vm.roles.length) { vm.type = "role"; next(); } else { vm.canRemove = false; - vm.loading = false; } }); } @@ -89,7 +92,8 @@ function save() { vm.buttonState = "busy"; var roles = _.map(vm.groups, function (group) { return group.name; }); - contentResource.updatePublicAccess(id, vm.userName, vm.password, roles, vm.loginPage.id, vm.errorPage.id).then( + var memberIds = _.map(vm.members, function (member) { return member.id; }); + contentResource.updatePublicAccess(id, roles, memberIds, vm.loginPage.id, vm.errorPage.id).then( function () { localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { vm.success = { @@ -143,6 +147,41 @@ vm.groups = _.without(vm.groups, group); } + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + _.each(model.selection, + function (member) { + if (!_.find(vm.members, function (m) { return m.id === member.id })) { + vm.members.push(member); + } + }); + } + editorService.close(); + navigationService.allowHideDialog(true); + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeMember(member) { + vm.members = _.without(vm.members, member); + } + function pickLoginPage() { pickPage(vm.loginPage); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 4b2bf54b5c..f179f1c63c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -20,11 +20,11 @@
- + -
@@ -40,16 +40,18 @@
-
-

Set the login and password for this page

+
+

Select the members that should have access to this page

- - - - - - - + + + + Add +
@@ -72,7 +74,7 @@
-
+

Select the pages that contain login form and error messages

diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index f30b4aa7bc..c0377e16ee 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -886,9 +886,9 @@ Mange hilsner fra Umbraco robotten %0%?]]> Vælg siderne der indeholder log ind-formularer og fejlmeddelelser %0%]]> - %0%]]> - Enkel brugerbeskyttelse - Hvis du blot ønsker at opsætte simpel beskyttelse ved hjælp af et enkelt login og kodeord + %0%]]> + Adgang til enkelte medlemmer + Hvis du ønsker at give adgang til enkelte medlemmer Udgivelsen kunne ikke udgives da publiceringsdato er sat diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 49111e9eab..f61186a98f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1137,10 +1137,10 @@ To manage your website, simply open the Umbraco back office and start adding con Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Single user protection - If you just want to setup simple protection using a single login and password + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Single user protection - If you just want to setup simple protection using a single login and password + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members Insufficient user permissions to publish all descendant documents diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index b7a33c3ab8..015354f0c6 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2173,9 +2173,12 @@ namespace Umbraco.Web.Editors // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) - var userName = entry.Rules - .FirstOrDefault(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) - ?.RuleValue; + var members = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) + .Select(rule => Services.MemberService.GetByUsername(rule.RuleValue)) + .Where(member => member != null) + .Select(Mapper.Map) + .ToArray(); var roles = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) .Select(rule => rule.RuleValue) @@ -2183,7 +2186,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK, new PublicAccess { - UserName = userName, + Members = members, Roles = roles, LoginPage = loginPageEntity != null ? Mapper.Map(loginPageEntity) : null, ErrorPage = errorPageEntity != null ? Mapper.Map(errorPageEntity) : null @@ -2193,9 +2196,9 @@ namespace Umbraco.Web.Editors // set up public access using role based access [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, int loginPageId, int errorPageId) + public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, [FromUri]int[] memberIds, int loginPageId, int errorPageId) { - if (roles == null || roles.Any() == false) + if ((roles == null || roles.Any() == false) && (userIds == null || userIds.Any() == false)) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } @@ -2208,14 +2211,23 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } + var isRoleBased = roles != null && roles.Any(); + var candidateRuleValues = isRoleBased + ? roles + : memberIds.Select(id => Services.MemberService.GetById(id)?.Username).Where(s => s != null).ToArray(); + var newRuleType = isRoleBased + ? Constants.Conventions.PublicAccess.MemberRoleRuleType + : Constants.Conventions.PublicAccess.MemberUsernameRuleType; + var entry = Services.PublicAccessService.GetEntryForContent(content); if (entry == null) { entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); - foreach (var role in roles) + + foreach (var role in candidateRuleValues) { - entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); + entry.AddRule(role, newRuleType); } } else @@ -2225,12 +2237,12 @@ namespace Umbraco.Web.Editors var currentRules = entry.Rules.ToArray(); var obsoleteRules = currentRules.Where(rule => - rule.RuleType != Constants.Conventions.PublicAccess.MemberRoleRuleType - || roles.Contains(rule.RuleValue) == false + rule.RuleType != newRuleType + || candidateRuleValues.Contains(rule.RuleValue) == false ); - var newRoles = roles.Where(group => + var newRuleValues = candidateRuleValues.Where(group => currentRules.Any(rule => - rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType + rule.RuleType == newRuleType && rule.RuleValue == group ) == false ); @@ -2238,9 +2250,9 @@ namespace Umbraco.Web.Editors { entry.RemoveRule(rule); } - foreach (var role in newRoles) + foreach (var role in newRuleValues) { - entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); + entry.AddRule(role, newRuleType); } } @@ -2249,30 +2261,6 @@ namespace Umbraco.Web.Editors : Request.CreateResponse(HttpStatusCode.InternalServerError); } - // set up public access using username and password - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] - [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, string userName, string password, int loginPageId, int errorPageId) - { - // TODO KJAC: validate password regex - if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) - { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); - } - - var content = Services.ContentService.GetById(contentId); - var loginPage = Services.ContentService.GetById(loginPageId); - var errorPage = Services.ContentService.GetById(errorPageId); - if (content == null || loginPage == null || errorPage == null) - { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); - } - - // TODO KJAC: implement - - return Request.CreateResponse(HttpStatusCode.OK); - } - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] public HttpResponseMessage RemovePublicAccess(int contentId) diff --git a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs index 68236d5934..b8035c9f25 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs @@ -5,8 +5,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "publicAccess", Namespace = "")] public class PublicAccess { - [DataMember(Name = "userName")] - public string UserName { get; set; } + //[DataMember(Name = "userName")] + //public string UserName { get; set; } [DataMember(Name = "roles")] public string[] Roles { get; set; } @@ -16,5 +16,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "errorPage")] public EntityBasic ErrorPage { get; set; } + + [DataMember(Name = "members")] + public MemberDisplay[] Members { get; set; } } } From a8b0ede770a5d68fc44c7f593c86a94c2a2d41c7 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 9 Dec 2018 15:03:35 +0100 Subject: [PATCH 14/33] Whoops forgot a file --- src/Umbraco.Web/Editors/ContentController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 015354f0c6..e245883ed7 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2198,7 +2198,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, [FromUri]int[] memberIds, int loginPageId, int errorPageId) { - if ((roles == null || roles.Any() == false) && (userIds == null || userIds.Any() == false)) + if ((roles == null || roles.Any() == false) && (memberIds == null || memberIds.Any() == false)) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } From efb9b37f5daac33ef753c46f0a708d0f0a930c53 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 09:27:03 +0100 Subject: [PATCH 15/33] Change from member IDs to usernames in member selections --- .../src/common/resources/content.resource.js | 8 ++--- .../content/content.protect.controller.js | 31 ++++++++++++++----- src/Umbraco.Web/Editors/ContentController.cs | 7 +++-- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 681a629ed0..75e1877be8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -991,13 +991,13 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * @param {Int} contentId The content Id * @param {Array} roles The roles that should have access (if using role based protection) - * @param {Array} memberIds The IDs of the members that should have access (if using member based protection) + * @param {Array} usernames The usernames of the members that should have access (if using member based protection) * @param {Int} loginPageId The Id of the login page * @param {Int} errorPageId The Id of the error page * @returns {Promise} resourcePromise object containing the public access protection * */ - updatePublicAccess: function (contentId, roles, memberIds, loginPageId, errorPageId) { + updatePublicAccess: function (contentId, roles, usernames, loginPageId, errorPageId) { var publicAccess = { contentId: contentId, loginPageId: loginPageId, @@ -1006,8 +1006,8 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { if (angular.isArray(roles) && roles.length) { publicAccess.roles = roles; } - else if (angular.isArray(memberIds) && memberIds.length) { - publicAccess.memberIds = memberIds; + else if (angular.isArray(usernames) && usernames.length) { + publicAccess.usernames = usernames; } else { throw "must supply either userName/password or roles"; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index ace37d4ac5..c6b6cecd5d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService, localizationService, editorService) { + function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { var vm = this; var id = $scope.currentNode.id; @@ -92,8 +92,8 @@ function save() { vm.buttonState = "busy"; var roles = _.map(vm.groups, function (group) { return group.name; }); - var memberIds = _.map(vm.members, function (member) { return member.id; }); - contentResource.updatePublicAccess(id, roles, memberIds, vm.loginPage.id, vm.errorPage.id).then( + var usernames = _.map(vm.members, function (member) { return member.username; }); + contentResource.updatePublicAccess(id, roles, usernames, vm.loginPage.id, vm.errorPage.id).then( function () { localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { vm.success = { @@ -161,15 +161,30 @@ filterCssClass: "not-allowed", submit: function (model) { if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames _.each(model.selection, function (member) { - if (!_.find(vm.members, function (m) { return m.id === member.id })) { - vm.members.push(member); - } + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function(newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); + } + }) + ); }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function() { + vm.loading = false; + }); } - editorService.close(); - navigationService.allowHideDialog(true); }, close: function () { editorService.close(); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index e245883ed7..423680fac5 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2173,6 +2173,7 @@ namespace Umbraco.Web.Editors // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) + // TODO: support custom membership providers here var members = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) .Select(rule => Services.MemberService.GetByUsername(rule.RuleValue)) @@ -2196,9 +2197,9 @@ namespace Umbraco.Web.Editors // set up public access using role based access [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, [FromUri]int[] memberIds, int loginPageId, int errorPageId) + public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, [FromUri]string[] usernames, int loginPageId, int errorPageId) { - if ((roles == null || roles.Any() == false) && (memberIds == null || memberIds.Any() == false)) + if ((roles == null || roles.Any() == false) && (usernames == null || usernames.Any() == false)) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } @@ -2214,7 +2215,7 @@ namespace Umbraco.Web.Editors var isRoleBased = roles != null && roles.Any(); var candidateRuleValues = isRoleBased ? roles - : memberIds.Select(id => Services.MemberService.GetById(id)?.Username).Where(s => s != null).ToArray(); + : usernames; var newRuleType = isRoleBased ? Constants.Conventions.PublicAccess.MemberRoleRuleType : Constants.Conventions.PublicAccess.MemberUsernameRuleType; From 35f0bf92f2efde5d3cf436c07e3ae08c034b117a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 09:55:35 +0100 Subject: [PATCH 16/33] Support custom membership providers (untested) --- src/Umbraco.Web/Editors/ContentController.cs | 33 ++++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 423680fac5..3f595f9cbc 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -8,6 +8,7 @@ using System.Text; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; +using System.Web.Security; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; @@ -2173,13 +2174,33 @@ namespace Umbraco.Web.Editors // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) - // TODO: support custom membership providers here - var members = entry.Rules + var usernames = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) - .Select(rule => Services.MemberService.GetByUsername(rule.RuleValue)) - .Where(member => member != null) - .Select(Mapper.Map) - .ToArray(); + .Select(rule => rule.RuleValue).ToArray(); + + MemberDisplay[] members; + switch (Services.MemberService.GetMembershipScenario()) + { + case MembershipScenario.NativeUmbraco: + members = usernames + .Select(username => Services.MemberService.GetByUsername(username)) + .Where(member => member != null) + .Select(Mapper.Map) + .ToArray(); + break; + // TODO: test support custom membership providers + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + members = usernames + .Select(username => provider.GetUser(username, false)) + .Where(membershipUser => membershipUser != null) + .Select(Mapper.Map) + .ToArray(); + break; + } + var roles = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) .Select(rule => rule.RuleValue) From 22b78ec617aaa404c385a70220ff0b0cd793bd52 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 11:18:16 +0100 Subject: [PATCH 17/33] Make sure at least one member is selected --- .../src/views/content/content.protect.controller.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index c6b6cecd5d..988cd10739 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -86,6 +86,9 @@ if (vm.type === "role") { return vm.groups && vm.groups.length > 0; } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } return true; } From e4f9c2916d52028bd15061a0f5ba42a3bf6c58cd Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 14:27:20 +0100 Subject: [PATCH 18/33] Use "groups", not "roles" + return richer group models from API --- .../src/common/resources/content.resource.js | 8 ++--- .../content/content.protect.controller.js | 27 +++++++++-------- .../src/views/content/protect.html | 16 +++++----- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 8 ++--- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 8 ++--- .../Umbraco/config/lang/en_us.xml | 8 ++--- src/Umbraco.Web/Editors/ContentController.cs | 29 ++++++++++--------- .../Models/ContentEditing/PublicAccess.cs | 7 ++--- 8 files changed, 55 insertions(+), 56 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index 75e1877be8..b807a4dc31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -990,21 +990,21 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} contentId The content Id - * @param {Array} roles The roles that should have access (if using role based protection) + * @param {Array} groups The names of the groups that should have access (if using group based protection) * @param {Array} usernames The usernames of the members that should have access (if using member based protection) * @param {Int} loginPageId The Id of the login page * @param {Int} errorPageId The Id of the error page * @returns {Promise} resourcePromise object containing the public access protection * */ - updatePublicAccess: function (contentId, roles, usernames, loginPageId, errorPageId) { + updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { var publicAccess = { contentId: contentId, loginPageId: loginPageId, errorPageId: errorPageId }; - if (angular.isArray(roles) && roles.length) { - publicAccess.roles = roles; + if (angular.isArray(groups) && groups.length) { + publicAccess.groups = groups; } else if (angular.isArray(usernames) && usernames.length) { publicAccess.usernames = usernames; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 988cd10739..8d80f308ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -36,7 +36,7 @@ // init the current settings for public access (if any) vm.loginPage = publicAccess.loginPage; vm.errorPage = publicAccess.errorPage; - vm.roles = publicAccess.roles || []; + vm.groups = publicAccess.groups || []; vm.members = publicAccess.members || []; vm.canRemove = true; @@ -44,8 +44,8 @@ vm.type = "member"; next(); } - else if (vm.roles.length) { - vm.type = "role"; + else if (vm.groups.length) { + vm.type = "group"; next(); } else { @@ -55,16 +55,14 @@ } function next() { - if (vm.type === "role") { + if (vm.type === "group") { vm.loading = true; - // Get all member groups + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore memberGroupResource.getGroups().then(function (groups) { vm.step = vm.type; vm.allGroups = groups; vm.hasGroups = groups.length > 0; - vm.groups = _.filter(groups, function(group) { - return _.contains(vm.roles, group.name); - }); vm.loading = false; }); } @@ -83,7 +81,7 @@ if (!vm.loginPage || !vm.errorPage) { return false; } - if (vm.type === "role") { + if (vm.type === "group") { return vm.groups && vm.groups.length > 0; } if (vm.type === "member") { @@ -94,9 +92,9 @@ function save() { vm.buttonState = "busy"; - var roles = _.map(vm.groups, function (group) { return group.name; }); + var groups = _.map(vm.groups, function (group) { return group.name; }); var usernames = _.map(vm.members, function (member) { return member.username; }); - contentResource.updatePublicAccess(id, roles, usernames, vm.loginPage.id, vm.errorPage.id).then( + contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( function () { localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { vm.success = { @@ -130,9 +128,10 @@ ? model.selectedMemberGroups : [model.selectedMemberGroup]; _.each(selectedGroupIds, - function(groupId) { + function (groupId) { + // find the group in the lookup list and add it if it isn't already var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); - if (group && !_.contains(vm.groups, group)) { + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { vm.groups.push(group); } }); @@ -147,7 +146,7 @@ } function removeGroup(group) { - vm.groups = _.without(vm.groups, group); + vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); } function pickMember() { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index f179f1c63c..ae4a15e8c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -29,11 +29,11 @@
- + -
@@ -55,12 +55,12 @@
-
-

You need to create a membergroup before you can use role-based authentication

+
+

You need to create a member group before you can use group based authentication

-
-

Pick the roles who have access to this page

+
+

Select the groups that should have access to this page

Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) - Rollebaseret beskyttelse - Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. - Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse + Gruppebaseret beskyttelse + Hvis du ønsker at give adgang til alle medlemmer af specifikke medlemsgrupper + Du skal oprette en medlemsgruppe før du kan bruge gruppebaseret beskyttelse Fejlside Brugt når folk er logget ind, men ingen adgang %0% skal beskyttes]]> @@ -885,7 +885,7 @@ Mange hilsner fra Umbraco robotten Fjern beskyttelse... %0%?]]> Vælg siderne der indeholder log ind-formularer og fejlmeddelelser - %0%]]> + %0%]]> %0%]]> Adgang til enkelte medlemmer Hvis du ønsker at give adgang til enkelte medlemmer diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index f61186a98f..51ea51685f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1124,9 +1124,9 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) - Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups - You need to create a membergroup before you can use role-based authentication + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication Error Page Used when people are logged on, but do not have access %0%]]> @@ -1137,7 +1137,7 @@ To manage your website, simply open the Umbraco back office and start adding con Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> + %0%]]> %0%]]> Specific members protection If you wish to grant access to specific members diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 1953a351be..b5efba5a32 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1146,9 +1146,9 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) - Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups - You need to create a membergroup before you can use role-based authentication + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication Error Page Used when people are logged on, but do not have access %0%]]> @@ -1159,7 +1159,7 @@ To manage your website, simply open the Umbraco back office and start adding con Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> + %0%]]> %0%]]> Specific members protection If you wish to grant access to specific members diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 3f595f9cbc..d1cb15e76d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -2201,15 +2201,18 @@ namespace Umbraco.Web.Editors break; } - var roles = entry.Rules + var allGroups = Services.MemberGroupService.GetAll().ToArray(); + var groups = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Select(rule => rule.RuleValue) + .Select(rule => allGroups.FirstOrDefault(g => g.Name == rule.RuleValue)) + .Where(memberGroup => memberGroup != null) + .Select(Mapper.Map) .ToArray(); return Request.CreateResponse(HttpStatusCode.OK, new PublicAccess { Members = members, - Roles = roles, + Groups = groups, LoginPage = loginPageEntity != null ? Mapper.Map(loginPageEntity) : null, ErrorPage = errorPageEntity != null ? Mapper.Map(errorPageEntity) : null }); @@ -2218,9 +2221,9 @@ namespace Umbraco.Web.Editors // set up public access using role based access [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, [FromUri]string[] usernames, int loginPageId, int errorPageId) + public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] groups, [FromUri]string[] usernames, int loginPageId, int errorPageId) { - if ((roles == null || roles.Any() == false) && (usernames == null || usernames.Any() == false)) + if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } @@ -2233,11 +2236,11 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } - var isRoleBased = roles != null && roles.Any(); - var candidateRuleValues = isRoleBased - ? roles + var isGroupBased = groups != null && groups.Any(); + var candidateRuleValues = isGroupBased + ? groups : usernames; - var newRuleType = isRoleBased + var newRuleType = isGroupBased ? Constants.Conventions.PublicAccess.MemberRoleRuleType : Constants.Conventions.PublicAccess.MemberUsernameRuleType; @@ -2247,9 +2250,9 @@ namespace Umbraco.Web.Editors { entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); - foreach (var role in candidateRuleValues) + foreach (var ruleValue in candidateRuleValues) { - entry.AddRule(role, newRuleType); + entry.AddRule(ruleValue, newRuleType); } } else @@ -2272,9 +2275,9 @@ namespace Umbraco.Web.Editors { entry.RemoveRule(rule); } - foreach (var role in newRuleValues) + foreach (var ruleValue in newRuleValues) { - entry.AddRule(role, newRuleType); + entry.AddRule(ruleValue, newRuleType); } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs index b8035c9f25..dcf2dcae92 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs @@ -5,11 +5,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "publicAccess", Namespace = "")] public class PublicAccess { - //[DataMember(Name = "userName")] - //public string UserName { get; set; } - - [DataMember(Name = "roles")] - public string[] Roles { get; set; } + [DataMember(Name = "groups")] + public MemberGroupDisplay[] Groups { get; set; } [DataMember(Name = "loginPage")] public EntityBasic LoginPage { get; set; } From 1ba41986d74ce3c4b7c9b2ba9a66e67bd0eec110 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 15:35:34 +0100 Subject: [PATCH 19/33] Make sure the public access supports member based access --- .../Services/PublicAccessServiceExtensions.cs | 17 ++++++++++++----- src/Umbraco.Web/Routing/PublishedRouter.cs | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 12db4daf40..b0dc979ebf 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web.Security; +using Umbraco.Core.Models; namespace Umbraco.Core.Services { @@ -41,7 +42,7 @@ namespace Umbraco.Core.Services return hasChange; } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, IEnumerable currentMemberRoles) + public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) { var content = contentService.GetById(documentId); if (content == null) return true; @@ -49,8 +50,7 @@ namespace Umbraco.Core.Services var entry = publicAccessService.GetEntryForContent(content); if (entry == null) return true; - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && currentMemberRoles.Contains(x.RuleValue)); + return HasAccess(entry, username, currentMemberRoles); } public static bool HasAccess(this IPublicAccessService publicAccessService, string path, MembershipUser member, RoleProvider roleProvider) @@ -77,8 +77,15 @@ namespace Umbraco.Core.Services var roles = rolesCallback(username); - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && roles.Contains(x.RuleValue)); + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + return entry.Rules.Any(x => + (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) + || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) + ); } } } diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 06c23406ab..1122aaa11a 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -637,7 +637,7 @@ namespace Umbraco.Web.Routing if (loginPageId != request.PublishedContent.Id) request.PublishedContent = request.UmbracoContext.PublishedSnapshot.Content.GetById(loginPageId); } - else if (_services.PublicAccessService.HasAccess(request.PublishedContent.Id, _services.ContentService, GetRolesForLogin(membershipHelper.CurrentUserName)) == false) + else if (_services.PublicAccessService.HasAccess(request.PublishedContent.Id, _services.ContentService, membershipHelper.CurrentUserName, GetRolesForLogin(membershipHelper.CurrentUserName)) == false) { _logger.Debug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); var errorPageId = publicAccessAttempt.Result.NoAccessNodeId; From 41624c8c16a6cdbfc6be9e038721316d7ed52c66 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 12 Dec 2018 23:07:41 +0100 Subject: [PATCH 20/33] Remove the old aspx page --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 9 +- .../Umbraco/dialogs/protectPage.aspx | 201 ----- .../Trees/LegacyTreeDataConverter.cs | 5 - src/Umbraco.Web/Umbraco.Web.csproj | 3 - .../umbraco/dialogs/protectPage.aspx.cs | 745 ------------------ 5 files changed, 4 insertions(+), 959 deletions(-) delete mode 100644 src/Umbraco.Web.UI/Umbraco/dialogs/protectPage.aspx delete mode 100644 src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2823908a92..963be029e5 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -336,7 +336,6 @@ - Designer @@ -452,10 +451,10 @@ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v11.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll - $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll - - $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll + $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll + + $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll diff --git a/src/Umbraco.Web.UI/Umbraco/dialogs/protectPage.aspx b/src/Umbraco.Web.UI/Umbraco/dialogs/protectPage.aspx deleted file mode 100644 index 5bcf97310f..0000000000 --- a/src/Umbraco.Web.UI/Umbraco/dialogs/protectPage.aspx +++ /dev/null @@ -1,201 +0,0 @@ -<%@ Page Language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" AutoEventWireup="True" Inherits="umbraco.presentation.umbraco.dialogs.protectPage" %> -<%@ Register TagPrefix="cc1" Namespace="Umbraco.Web._Legacy.Controls" Assembly="Umbraco.Web" %> - - - - - - - - - - - - -
- -
- -
- - - -
- - - -
- - - -
- - -
- - - -
- -
-
- - -
- - -
- - -
- - - - -
- -
- - - -
- - -

Member name already exists, click Change to use a different name or Update to continue

-
-
- - - -

<%= Services.TextService.Localize("publicAccess/paSelectRoles")%>

-
- - - -
- - - - - - <%=Services.TextService.Localize("paLoginPageHelp")%> - -
- -
- - -
- - - - - <%=Services.TextService.Localize("paErrorPageHelp")%> - -
- -
- -
- -
-
- -
- - -
- - - - - - - diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs index 85cebdf3f5..1df9127b8e 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs @@ -55,11 +55,6 @@ namespace Umbraco.Web.Trees new LegacyUrlAction( "create.aspx?nodeId=" + nodeId + "&nodeType=" + nodeType + "&nodeName=" + nodeName + "&rnd=" + DateTime.UtcNow.Ticks, Current.Services.TextService.Localize("actions/create"))); - case ActionProtect actionProtect: - return Attempt.Succeed( - new LegacyUrlAction( - "dialogs/protectPage.aspx?mode=cut&nodeId=" + nodeId + "&rnd=" + DateTime.UtcNow.Ticks, - Current.Services.TextService.Localize("actions/protect"))); } return Attempt.Fail(); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 132d65c520..79ee2dbbb5 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1012,9 +1012,6 @@ ASPXCodeBehind - - ASPXCodeBehind - diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs deleted file mode 100644 index 07d13230e5..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/protectPage.aspx.cs +++ /dev/null @@ -1,745 +0,0 @@ -using Umbraco.Core.Services; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web; -using System.Web.Security; -using System.Web.UI.WebControls; -using Umbraco.Core; -using Umbraco.Core.Logging; -using umbraco.controls; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Web; -using Umbraco.Web.Composing; -using Umbraco.Web.UI.Pages; -using MembershipProviderExtensions = Umbraco.Core.Security.MembershipProviderExtensions; - -namespace umbraco.presentation.umbraco.dialogs -{ - /// - /// Summary description for protectPage. - /// - public partial class protectPage : UmbracoEnsuredPage - { - public protectPage() - { - CurrentApp = Constants.Applications.Content.ToString(); - } - - protected Literal jsShowWindow; - protected DualSelectbox _memberGroups = new DualSelectbox(); - protected ContentPicker loginPagePicker = new ContentPicker(); - protected ContentPicker errorPagePicker = new ContentPicker(); - - private const string MemberIdRuleType = "MemberId"; // moved from Constants-Conventions.cs - - - override protected void OnInit(EventArgs e) - { - base.OnInit(e); - } - - protected void selectMode(object sender, EventArgs e) - { - p_mode.Visible = false; - p_setup.Visible = true; - - if (rb_simple.Checked) - { - pane_advanced.Visible = false; - pane_simple.Visible = true; - bt_protect.CommandName = "simple"; - } - else - { - pane_advanced.Visible = true; - pane_simple.Visible = false; - bt_protect.CommandName = "advanced"; - } - } - - private ProtectionType GetProtectionType(int documentId) - { - var content = Services.ContentService.GetById(documentId); - if (content == null) return ProtectionType.NotProtected; - - var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) return ProtectionType.NotProtected; - - //legacy states that if it is protected by a member id then it is 'simple' - return entry.Rules.Any(x => x.RuleType == MemberIdRuleType) - ? ProtectionType.Simple - : ProtectionType.Advanced; - } - - private enum ProtectionType - { - NotProtected, - Simple, - Advanced - } - - private int GetErrorPage(string path) - { - var entry = Services.PublicAccessService.GetEntryForContent(path); - if (entry == null) return -1; - var entity = Services.EntityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document, false); - return entity.Id; - } - - private int GetLoginPage(string path) - { - var entry = Services.PublicAccessService.GetEntryForContent(path); - if (entry == null) return -1; - var entity = Services.EntityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document, false); - return entity.Id; - } - - private MembershipUser GetAccessingMembershipUser(int documentId) - { - var content = Services.ContentService.GetById(documentId); - if (content == null) return null; - var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) return null; - //legacy would throw an exception here if it was not 'simple' and simple means based on a username - if (entry.Rules.All(x => x.RuleType != Constants.Conventions.PublicAccess.MemberUsernameRuleType)) - { - throw new Exception("Document isn't protected using Simple mechanism. Use GetAccessingMemberGroups instead"); - } - var provider = MembershipProviderExtensions.GetMembersMembershipProvider(); - var usernameRule = entry.Rules.First(x => x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType); - return provider.GetUser(usernameRule.RuleValue, false); - } - - private bool IsProtectedByMembershipRole(int documentId, string role) - { - var content = Services.ContentService.GetById(documentId); - var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry == null) return false; - return entry.Rules - .Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && x.RuleValue == role); - } - - private void ProtectPage(bool Simple, int DocumentId, int LoginDocumentId, int ErrorDocumentId) - { - var content = Current.Services.ContentService.GetById(DocumentId); - var loginContent = Services.ContentService.GetById(LoginDocumentId); - if (loginContent == null) throw new NullReferenceException("No content item found with id " + LoginDocumentId); - var noAccessContent = Services.ContentService.GetById(ErrorDocumentId); - if (noAccessContent == null) throw new NullReferenceException("No content item found with id " + ErrorDocumentId); - - var entry = Services.PublicAccessService.GetEntryForContent(content.Id.ToString()); - if (entry != null) - { - if (Simple) - { - // if using simple mode, make sure that all existing groups are removed - entry.ClearRules(); - } - - //ensure the correct ids are applied - entry.LoginNodeId = loginContent.Id; - entry.NoAccessNodeId = noAccessContent.Id; - } - else - { - entry = new PublicAccessEntry(content, - Services.ContentService.GetById(LoginDocumentId), - Services.ContentService.GetById(ErrorDocumentId), - new List()); - } - Services.PublicAccessService.Save(entry); - } - - private void AddMembershipRoleToDocument(int documentId, string role) - { - //event - var content = Current.Services.ContentService.GetById(documentId); - - var entry = Services.PublicAccessService.AddRule( - content, - Constants.Conventions.PublicAccess.MemberRoleRuleType, - role); - - if (entry.Success == false && entry.Result.Entity == null) - { - throw new Exception("Document is not protected!"); - } - } - - private void AddMembershipUserToDocument(int documentId, string membershipUserName) - { - //event - var content = Current.Services.ContentService.GetById(documentId); - var entry = Services.PublicAccessService.AddRule( - content, - Constants.Conventions.PublicAccess.MemberUsernameRuleType, - membershipUserName); - - if (entry.Success == false && entry.Result.Entity == null) - { - throw new Exception("Document is not protected!"); - } - } - - private void RemoveMembershipRoleFromDocument(int documentId, string role) - { - var content = Current.Services.ContentService.GetById(documentId); - Services.PublicAccessService.RemoveRule( - content, - Constants.Conventions.PublicAccess.MemberRoleRuleType, - role); - } - - private void RemoveProtection(int documentId) - { - var content = Current.Services.ContentService.GetById(documentId); - var entry = Services.PublicAccessService.GetEntryForContent(content); - if (entry != null) - { - Services.PublicAccessService.Delete(entry); - } - } - - protected void Page_Load(object sender, EventArgs e) - { - // Check for editing - int documentId = int.Parse(Request.GetItemAsString("nodeId")); - var content = Current.Services.ContentService.GetById(documentId); - jsShowWindow.Text = ""; - - ph_errorpage.Controls.Add(errorPagePicker); - ph_loginpage.Controls.Add(loginPagePicker); - - pp_login.Text = Services.TextService.Localize("login"); - pp_pass.Text = Services.TextService.Localize("password"); - pp_loginPage.Text = Services.TextService.Localize("paLoginPage"); - pp_errorPage.Text = Services.TextService.Localize("paErrorPage"); - - pane_chooseMode.Text = Services.TextService.Localize("publicAccess/paHowWould"); - pane_pages.Text = Services.TextService.Localize("publicAccess/paSelectPages"); - pane_simple.Text = Services.TextService.Localize("publicAccess/paSimple"); - pane_advanced.Text = Services.TextService.Localize("publicAccess/paAdvanced"); - - if (IsPostBack == false) - { - if (Services.PublicAccessService.IsProtected(documentId.ToString()) - && GetProtectionType(documentId) != ProtectionType.NotProtected) - { - bt_buttonRemoveProtection.Visible = true; - bt_buttonRemoveProtection.Attributes.Add("onClick", "return confirm('" + Services.TextService.Localize("areyousure") + "')"); - - // Get login and error pages - int errorPage = GetErrorPage(content.Path); - int loginPage = GetLoginPage(content.Path); - try - { - var loginPageContent = Current.Services.ContentService.GetById(loginPage); - if (loginPageContent != null) - { - loginPagePicker.Value = loginPage.ToString(CultureInfo.InvariantCulture); - } - var errorPageContent = Current.Services.ContentService.GetById(errorPage); - errorPagePicker.Value = errorPage.ToString(CultureInfo.InvariantCulture); - } - catch (Exception ex) - { - Current.Logger.Error(ex, "An error occurred initializing the protect page editor"); - } - - if (GetProtectionType(documentId) == ProtectionType.Simple) - { - MembershipUser m = GetAccessingMembershipUser(documentId); - if (m != null) - { - pane_simple.Visible = true; - pp_pass.Visible = false; - simpleLogin.Visible = false; - SimpleLoginLabel.Visible = true; - SimpleLoginLabel.Text = m.UserName; - pane_advanced.Visible = false; - bt_protect.CommandName = "simple"; - } - - } - else if (GetProtectionType(documentId) == ProtectionType.Advanced) - { - pane_simple.Visible = false; - pane_advanced.Visible = true; - bt_protect.CommandName = "advanced"; - } - - p_mode.Visible = false; - p_setup.Visible = true; - } - } - - // Load up membergrouops - _memberGroups.ID = "Membergroups"; - _memberGroups.Width = 175; - _memberGroups.Height = 165; - var selectedGroups = ""; - - // get roles from the membership provider - var roles = Roles.GetAllRoles().OrderBy(x => x).ToArray(); - - if (roles.Length > 0) - { - foreach (var role in roles) - { - var listItem = new ListItem(role, role); - _memberGroups.Items.Add(listItem); - if (IsPostBack) continue; - - // first time, initialize selected roles - if (IsProtectedByMembershipRole(documentId, role)) - selectedGroups += role + ","; - } - } - else - { - p_noGroupsFound.Visible = true; - rb_advanced.Enabled = false; - } - - _memberGroups.Value = selectedGroups; - groupsSelector.Controls.Add(_memberGroups); - - - bt_protect.Text = Services.TextService.Localize("buttons", "select"); - bt_buttonRemoveProtection.Text = Services.TextService.Localize("paRemoveProtection"); - - // Put user code to initialize the page here - } - - protected void ChangeOnClick(object sender, EventArgs e) - { - SimpleLoginNameValidator.IsValid = true; - SimpleLoginLabel.Visible = false; - simpleLogin.Visible = true; - pp_pass.Visible = true; - } - - protected void protect_Click(object sender, CommandEventArgs e) - { - if (string.IsNullOrEmpty(errorPagePicker.Value) || errorPagePicker.Value == "-1") - cv_errorPage.IsValid = false; - - if (string.IsNullOrEmpty(loginPagePicker.Value) || loginPagePicker.Value == "-1") - cv_loginPage.IsValid = false; - - //reset - SimpleLoginNameValidator.IsValid = true; - - var provider = Umbraco.Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); - var pageId = int.Parse(Request.GetItemAsString("nodeId")); - - if (e.CommandName == "simple") - { - var memberLogin = simpleLogin.Visible ? simpleLogin.Text : SimpleLoginLabel.Text; - - var member = provider.GetUser(memberLogin, false); - if (member == null) - { - var tempEmail = "u" + Guid.NewGuid().ToString("N") + "@example.com"; - - // this needs to work differently depending on umbraco members or external membership provider - if (provider.IsUmbracoMembershipProvider() == false) - { - member = provider.CreateUser(memberLogin, simplePassword.Text, tempEmail); - } - else - { - //if it's the umbraco membership provider, then we need to tell it what member type to create it with - if (Current.Services.MemberTypeService.Get(Constants.Conventions.MemberTypes.SystemDefaultProtectType) == null) - //if (MemberType.GetByAlias(Constants.Conventions.MemberTypes.SystemDefaultProtectType) == null) - { - var defm = new MemberType(null, Constants.Conventions.MemberTypes.SystemDefaultProtectType); - Current.Services.MemberTypeService.Save(defm); - } - var castedProvider = provider.AsUmbracoMembershipProvider(); - MembershipCreateStatus status; - member = castedProvider.CreateUser(Constants.Conventions.MemberTypes.SystemDefaultProtectType, - memberLogin, simplePassword.Text, tempEmail, null, null, true, null, out status); - if (status != MembershipCreateStatus.Success) - { - SimpleLoginNameValidator.IsValid = false; - SimpleLoginNameValidator.ErrorMessage = "Could not create user: " + status; - SimpleLoginNameValidator.Text = "Could not create user: " + status; - return; - } - } - } - else if (pp_pass.Visible) - { - SimpleLoginNameValidator.IsValid = false; - SimpleLoginLabel.Visible = true; - SimpleLoginLabel.Text = memberLogin; - simpleLogin.Visible = false; - pp_pass.Visible = false; - return; - } - - // Create or find a memberGroup - var simpleRoleName = "__umbracoRole_" + member.UserName; - if (Roles.RoleExists(simpleRoleName) == false) - { - Roles.CreateRole(simpleRoleName); - } - if (Roles.IsUserInRole(member.UserName, simpleRoleName) == false) - { - Roles.AddUserToRole(member.UserName, simpleRoleName); - } - - ProtectPage(true, pageId, int.Parse(loginPagePicker.Value), int.Parse(errorPagePicker.Value)); - AddMembershipRoleToDocument(pageId, simpleRoleName); - AddMembershipUserToDocument(pageId, member.UserName); - } - else if (e.CommandName == "advanced") - { - if (cv_errorPage.IsValid && cv_loginPage.IsValid) - { - ProtectPage(false, pageId, int.Parse(loginPagePicker.Value), int.Parse(errorPagePicker.Value)); - - foreach (ListItem li in _memberGroups.Items) - if (("," + _memberGroups.Value + ",").IndexOf("," + li.Value + ",", StringComparison.Ordinal) > -1) - AddMembershipRoleToDocument(pageId, li.Value); - else - RemoveMembershipRoleFromDocument(pageId, li.Value); - } - else - { - return; - } - } - - var content = Services.ContentService.GetById(pageId); - var text = content == null ? "" : content.Name; - feedback_text.Text = HttpUtility.HtmlEncode(Services.TextService.Localize("publicAccess/paIsProtected", new[] { text })); - - p_setup.Visible = false; - p_feedback.Visible = true; - - //reloads the current node in the tree - ClientTools.SyncTree(content.Path, true); - //reloads the current node's children in the tree - ClientTools.ReloadActionNode(false, true); - } - - - protected void buttonRemoveProtection_Click(object sender, System.EventArgs e) - { - int pageId = int.Parse(Request.GetItemAsString("nodeId")); - p_setup.Visible = false; - - RemoveProtection(pageId); - - var content = Services.ContentService.GetById(pageId); - var text = content == null ? "" : content.Name; - feedback_text.Text = HttpUtility.HtmlEncode(Services.TextService.Localize("publicAccess/paIsRemoved", new[] { text })); - p_feedback.Visible = true; - - - //reloads the current node in the tree - ClientTools.SyncTree(content.Path, true); - //reloads the current node's children in the tree - ClientTools.ReloadActionNode(false, true); - } - - protected CustomValidator SimpleLoginNameValidator; - protected Label SimpleLoginLabel; - - /// - /// tempFile control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlInputHidden tempFile; - - /// - /// p_feedback control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel p_feedback; - - /// - /// p_mode control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel p_mode; - - /// - /// p_setup control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel p_setup; - - /// - /// pane_chooseMode control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane pane_chooseMode; - - /// - /// rb_simple control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RadioButton rb_simple; - - /// - /// rb_advanced control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RadioButton rb_advanced; - - /// - /// p_noGroupsFound control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel p_noGroupsFound; - - /// - /// bt_selectMode control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_selectMode; - - /// - /// pane_simple control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane pane_simple; - - /// - /// PropertyPanel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel1; - - /// - /// pp_login control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel pp_login; - - /// - /// simpleLogin control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox simpleLogin; - - /// - /// pp_pass control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel pp_pass; - - /// - /// simplePassword control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox simplePassword; - - /// - /// pane_advanced control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane pane_advanced; - - /// - /// PropertyPanel3 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel3; - - /// - /// PropertyPanel2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel2; - - /// - /// groupsSelector control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder groupsSelector; - - /// - /// pane_pages control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane pane_pages; - - /// - /// pp_loginPage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel pp_loginPage; - - /// - /// ph_loginpage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder ph_loginpage; - - /// - /// cv_loginPage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CustomValidator cv_loginPage; - - /// - /// pp_errorPage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel pp_errorPage; - - /// - /// ph_errorpage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder ph_errorpage; - - /// - /// cv_errorPage control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CustomValidator cv_errorPage; - - /// - /// bt_protect control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_protect; - - /// - /// bt_buttonRemoveProtection control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button bt_buttonRemoveProtection; - - /// - /// errorId control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlInputHidden errorId; - - /// - /// loginId control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.HtmlControls.HtmlInputHidden loginId; - - /// - /// js control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder js; - - /// - /// feedback_text control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Literal feedback_text; - - - } -} From b2a1b11a5b6e230dd0d5a235de3f620f766120f9 Mon Sep 17 00:00:00 2001 From: Brian Juul Andersen Date: Sat, 29 Dec 2018 05:58:45 +0100 Subject: [PATCH 21/33] Fixed minor grammatical error --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6caeadd0e5..9dc6f9457f 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,7 +16,7 @@ This document gives you a quick overview on how to get started, we will link to ## Guidelines for contributions we welcome -Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time. +Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes. From debbf87a047ee89902b8e938ddfdacf61d86714e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 31 Dec 2018 12:50:27 +0100 Subject: [PATCH 22/33] Support image crop data set when overwriting files using the API --- src/Umbraco.Core/IO/MediaFileSystem.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index 37fcdeba70..39cd7ba7ad 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,4 +1,6 @@ -using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; @@ -293,6 +295,12 @@ namespace Umbraco.Core.IO { var property = GetProperty(content, propertyTypeAlias); var svalue = property.Value as string; + if (svalue != null && svalue.DetectIsJson()) + { + // the property value is a JSON serialized image crop data set - grab the "src" property as the file source + var jObject = JsonConvert.DeserializeObject(svalue); + svalue = jObject != null ? jObject.GetValueAsString("src") : svalue; + } var oldpath = svalue == null ? null : GetRelativePath(svalue); var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); property.Value = GetUrl(filepath); From b852c78861fe50c45a39126d1e881f11e35189fe Mon Sep 17 00:00:00 2001 From: Callum Whyte Date: Fri, 21 Dec 2018 22:17:47 +0000 Subject: [PATCH 23/33] Fixing spelling mistakes in helveticons CSS class names The icon picker uses these class names as labels, kept the old class names for backwards compatibility --- src/Umbraco.Web.UI.Client/src/less/helveticons.less | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/helveticons.less b/src/Umbraco.Web.UI.Client/src/less/helveticons.less index 9a317e09fb..fab9614c7e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/helveticons.less +++ b/src/Umbraco.Web.UI.Client/src/less/helveticons.less @@ -17,12 +17,14 @@ -webkit-font-smoothing: antialiased; *margin-right: .3em; } + [class^="icon-"]:before, [class*=" icon-"]:before { text-decoration: inherit; display: inline-block; speak: none; } + /* [class^="icon-"]:before, [class*=" icon-"]:before { font-family: 'icomoon'; @@ -38,7 +40,6 @@ i.large{ font-size: 32px; } - i.medium{ font-size: 24px; } @@ -187,8 +188,6 @@ i.small{ .icon-umb-translation:before, .traytranslation:before { content: "\e1fd"; } - - .icon-tv:before { content: "\e02e"; } @@ -213,7 +212,8 @@ i.small{ .icon-train:before { content: "\e035"; } -.icon-trafic:before { +.icon-trafic:before, +.icon-traffic:before { content: "\e036"; } .icon-traffic-alt:before { @@ -255,6 +255,7 @@ i.small{ .icon-target:before { content: "\e043"; } +.icon-temperature-alt:before, .icon-temperatrure-alt:before { content: "\e044"; } @@ -267,6 +268,7 @@ i.small{ .icon-theater:before { content: "\e047"; } +.icon-thief:before, .icon-theif:before { content: "\e048"; } @@ -375,6 +377,7 @@ i.small{ .icon-shuffle:before { content: "\e06b"; } +.icon-science:before, .icon-sience:before { content: "\e06c"; } @@ -747,6 +750,7 @@ i.small{ .icon-pictures-alt-2:before { content: "\e0e7"; } +.icon-panel-close:before, .icon-pannel-close:before { content: "\e0e8"; } @@ -1627,6 +1631,7 @@ i.small{ .icon-alarm-clock:before { content: "\e20c"; } +.icon-addressbook:before, .icon-adressbook:before { content: "\e20d"; } From 0ce54847b046d0e1c55e276418ffebef5c7e550e Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 20 Dec 2018 16:21:15 +0100 Subject: [PATCH 24/33] #3916 mapping to a IEnumerable resulted in all items in the result list being the same. Now map items indiviually. Could be caused by this : https://stackoverflow.com/questions/17268362/automapper-for-a-list-scenario-only-seems-to-repeat-mapping-the-first-object-in --- src/Umbraco.Web/Editors/LogController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index 04e34e91e0..444729b252 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Editors long totalRecords; var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null; var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); - var mapped = Mapper.Map>(result); + var mapped = result.Select(item => Mapper.Map(item)); // Mapper.Map>(result); var page = new PagedResult(totalRecords, pageNumber, pageSize) { From ee8b4c58847b2363db1fae90c5dc499664e2725d Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Thu, 20 Dec 2018 16:28:24 +0100 Subject: [PATCH 25/33] #3916 fix multiple enumerations of ienumerable...so usernames and avatars are displayed correctly --- src/Umbraco.Web/Editors/LogController.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index 444729b252..b02d49209e 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -85,16 +85,17 @@ namespace Umbraco.Web.Editors private IEnumerable MapAvatarsAndNames(IEnumerable items) { - var userIds = items.Select(x => x.UserId).ToArray(); + var mappedItems = items.ToList(); + var userIds = mappedItems.Select(x => x.UserId).ToArray(); var users = Services.UserService.GetUsersById(userIds) .ToDictionary(x => x.Id, x => x.GetUserAvatarUrls(ApplicationContext.ApplicationCache.RuntimeCache)); var userNames = Services.UserService.GetUsersById(userIds).ToDictionary(x => x.Id, x => x.Name); - foreach (var item in items) + foreach (var item in mappedItems) { item.UserAvatars = users[item.UserId]; item.UserName = userNames[item.UserId]; } - return items; + return mappedItems; } } } From 5e680e80e6b766f02922a30fcb9f7294d7866e61 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 31 Dec 2018 13:48:34 +0100 Subject: [PATCH 26/33] Deletes commented out code --- src/Umbraco.Web/Editors/LogController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/LogController.cs b/src/Umbraco.Web/Editors/LogController.cs index b02d49209e..c54bd989d4 100644 --- a/src/Umbraco.Web/Editors/LogController.cs +++ b/src/Umbraco.Web/Editors/LogController.cs @@ -27,7 +27,7 @@ namespace Umbraco.Web.Editors long totalRecords; var dateQuery = sinceDate.HasValue ? Query.Builder.Where(x => x.CreateDate >= sinceDate) : null; var result = Services.AuditService.GetPagedItemsByEntity(id, pageNumber - 1, pageSize, out totalRecords, orderDirection, customFilter: dateQuery); - var mapped = result.Select(item => Mapper.Map(item)); // Mapper.Map>(result); + var mapped = result.Select(item => Mapper.Map(item)); var page = new PagedResult(totalRecords, pageNumber, pageSize) { From af2f531d315b5d82ae18bce5bc3b823290da83c3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 31 Dec 2018 14:37:58 +0100 Subject: [PATCH 27/33] Add missing localization of dialogs (#3844) --- .../src/views/content/content.notify.controller.js | 9 +++++++-- .../src/views/content/copy.html | 7 +++++-- .../src/views/content/move.html | 4 +++- .../src/views/content/notify.html | 4 ++-- src/Umbraco.Web.UI.Client/src/views/media/move.html | 4 +++- src/Umbraco.Web.UI/umbraco/config/lang/da.xml | 9 +++++++-- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 9 +++++++-- src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 13 +++++++------ src/Umbraco.Web.UI/umbraco/dialogs/rollBack.aspx | 2 +- .../umbraco/dialogs/rollBack.aspx.cs | 5 +++++ 10 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js index c5b2781dd4..04a3a76027 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.notify.controller.js @@ -3,7 +3,8 @@ $scope, contentResource, navigationService, - angularHelper) { + angularHelper, + localizationService) { var vm = this; var currentForm; vm.notifyOptions = []; @@ -11,7 +12,8 @@ vm.cancel = cancel; vm.message = { name: $scope.currentNode.name - };; + }; + vm.labels = {}; function onInit() { vm.loading = true; contentResource.getNotifySettingsById($scope.currentNode.id).then(function (options) { @@ -19,6 +21,9 @@ vm.loading = false; vm.notifyOptions = options; }); + localizationService.localize("notifications_editNotifications", [$scope.currentNode.name]).then(function(value) { + vm.labels.headline = value; + }); } function cancel() { navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 62116a3e6e..963a0e5df2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -11,14 +11,17 @@
- {{currentNode.name}} was copied to + {{currentNode.name}} + was copied to {{target.name}}

- Choose where to copy {{currentNode.name}} to in the tree structure below + Choose where to copy + {{currentNode.name}} + to in the tree structure below

diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index 09116a2a6d..fa6dee2ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -11,7 +11,9 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{currentNode.name}} + was moved to + {{target.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/notify.html b/src/Umbraco.Web.UI.Client/src/views/content/notify.html index e7c4d4d785..8e2f860661 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/notify.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/notify.html @@ -13,13 +13,13 @@
- {{currentNode.name}} + {{currentNode.name}}
- Set your notification for {{ currentNode.name }} +
- {{currentNode.name}} was moved underneath {{target.name}} + {{currentNode.name}} + was moved to + {{target.name}}
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 4f61bf3ee4..658dd091f6 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -32,8 +32,11 @@ Omdøb Gendan Sæt rettigheder for siden %0% + Vælg hvor du vil kopiere Vælg hvortil du vil flytte - I træstrukturen nedenfor + til i træstrukturen nedenfor + blev flyttet til + blev kopieret til Rettigheder Fortryd ændringer Send til udgivelse @@ -830,7 +833,8 @@ Relater det kopierede element til originalen - Rediger dine notificeringer for %0% + %0%]]> + Notificeringer er gemt for + Vælg en version at sammenligne med den nuværende version Nuværende version Rød tekst vil ikke blive vist i den valgte version. Grøn betyder tilføjet]]> Dokument tilbagerullet diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 76cf013b40..cd5f32da0d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -33,8 +33,11 @@ Rename Restore Set permissions for the page %0% + Choose where to copy Choose where to move - In the tree structure below + to in the tree structure below + was moved to + was copied to Permissions Rollback Send To Publish @@ -1038,7 +1041,8 @@ To manage your website, simply open the Umbraco back office and start adding con Relate copied items to original - Edit your notification for %0% + %0%]]> + Notification settings saved for Undo edits + Select a version to compare with the current version Current version Red text will not be shown in the selected version. , green means added]]> Document has been rolled back diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 47fef59019..88b736a34f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -33,8 +33,11 @@ Rename Restore Set permissions for the page %0% + Choose where to copy Choose where to move - In the tree structure below + to in the tree structure below + was moved to + was copied to Permissions Rollback Send To Publish @@ -1037,7 +1040,8 @@ To manage your website, simply open the Umbraco back office and start adding con Relate copied items to original - Edit your notification for %0% + %0%]]> + Notification settings saved for Undo edits + Select a version to compare with the current version Current version Red text will not be shown in the selected version. , green means added]]> Document has been rolled back @@ -2220,8 +2225,4 @@ To manage your website, simply open the Umbraco back office and start adding con There is no location where this item can be automatically restored. You can move the item manually using the tree below. was restored under - - Select your notifications for - Notification settings saved for - diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/rollBack.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/rollBack.aspx index f596121627..144a840ca5 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/rollBack.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/rollBack.aspx @@ -55,7 +55,7 @@
diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/rollBack.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/rollBack.aspx.cs index f6390a9d0b..543c3f8b23 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/rollBack.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/rollBack.aspx.cs @@ -116,6 +116,11 @@ namespace umbraco.presentation.dialogs currentVersionTitle.Text = currentDoc.Text; currentVersionMeta.Text = ui.Text("content", "createDate") + ": " + currentDoc.VersionDate.ToShortDateString() + " " + currentDoc.VersionDate.ToShortTimeString(); + pp_selectVersion.Text = ui.Text("rollback", "headline"); + pp_currentVersion.Text = ui.Text("rollback", "currentVersion"); + pp_view.Text = ui.Text("rollback", "view"); + pp_rollBackTo.Text = ui.Text("rollback", "rollbackTo"); + if (!IsPostBack) { allVersions.Items.Add(new ListItem(ui.Text("rollback", "selectVersion")+ "...", "")); From 0150b972780cfa39cce58003a92f0f5b9909fb10 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 31 Dec 2018 15:02:59 +0100 Subject: [PATCH 28/33] Show password change validation errors at the correct fields (#3918) * Show password change validation errors at the correct password fields + remove the redundant "Could not reset password, errors: " message text * Explicitly find the user instead of mapping it. --- src/Umbraco.Web/Editors/PasswordChanger.cs | 37 ++++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web/Editors/PasswordChanger.cs b/src/Umbraco.Web/Editors/PasswordChanger.cs index 7be14d27d1..266af76454 100644 --- a/src/Umbraco.Web/Editors/PasswordChanger.cs +++ b/src/Umbraco.Web/Editors/PasswordChanger.cs @@ -100,7 +100,7 @@ namespace Umbraco.Web.Editors { var errors = string.Join(". ", resetResult.Errors); _logger.Warn(string.Format("Could not reset user password {0}", errors)); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) }); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "resetPassword" }) }); } return Attempt.Succeed(new PasswordChangedModel()); @@ -120,21 +120,30 @@ namespace Umbraco.Web.Editors return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) }); } - if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false) + //get the user + var backOfficeIdentityUser = await userMgr.FindByIdAsync(savingUser.Id); + if (backOfficeIdentityUser == null) { - //if an old password is suplied try to change it - var changeResult = await userMgr.ChangePasswordAsync(savingUser.Id, passwordModel.OldPassword, passwordModel.NewPassword); - if (changeResult.Succeeded == false) - { - var errors = string.Join(". ", changeResult.Errors); - _logger.Warn(string.Format("Could not change user password {0}", errors)); - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "oldPassword" }) }); - } - return Attempt.Succeed(new PasswordChangedModel()); + //this really shouldn't ever happen... but just in case + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password could not be verified", new[] { "oldPassword" }) }); } - - //We shouldn't really get here - return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid information supplied", new[] { "value" }) }); + //is the old password correct? + var validateResult = await userMgr.CheckPasswordAsync(backOfficeIdentityUser, passwordModel.OldPassword); + if(validateResult == false) + { + //no, fail with an error message for "oldPassword" + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Incorrect password", new[] { "oldPassword" }) }); + } + //can we change to the new password? + var changeResult = await userMgr.ChangePasswordAsync(savingUser.Id, passwordModel.OldPassword, passwordModel.NewPassword); + if (changeResult.Succeeded == false) + { + //no, fail with error messages for "password" + var errors = string.Join(". ", changeResult.Errors); + _logger.Warn(string.Format("Could not change user password {0}", errors)); + return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult(errors, new[] { "password" }) }); + } + return Attempt.Succeed(new PasswordChangedModel()); } /// From ebf15b905280f7d542f89b528ba2a5b17e93f2b8 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 2 Jan 2019 15:24:05 +0100 Subject: [PATCH 29/33] Fix CacheRefresher and DistributedCache --- ...ests.cs => DistributedCacheBinderTests.cs} | 40 ++++- .../Integration/ContentEventsTests.cs | 12 +- .../Scoping/ScopedNuCacheTests.cs | 11 +- .../Scoping/ScopedRepositoryTests.cs | 19 +-- src/Umbraco.Tests/Scoping/ScopedXmlTests.cs | 15 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 2 +- .../Cache/DistributedCacheBinder.cs | 85 +++++++++++ .../Cache/DistributedCacheBinderComponent.cs | 24 +++ ....cs => DistributedCacheBinder_Handlers.cs} | 138 +++++------------- .../Cache/IDistributedCacheBinder.cs | 34 +++++ src/Umbraco.Web/Services/SectionService.cs | 12 +- src/Umbraco.Web/Umbraco.Web.csproj | 5 +- 12 files changed, 253 insertions(+), 144 deletions(-) rename src/Umbraco.Tests/Cache/{CacheRefresherComponentTests.cs => DistributedCacheBinderTests.cs} (87%) create mode 100644 src/Umbraco.Web/Cache/DistributedCacheBinder.cs create mode 100644 src/Umbraco.Web/Cache/DistributedCacheBinderComponent.cs rename src/Umbraco.Web/Cache/{CacheRefresherComponent.cs => DistributedCacheBinder_Handlers.cs} (86%) create mode 100644 src/Umbraco.Web/Cache/IDistributedCacheBinder.cs diff --git a/src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs similarity index 87% rename from src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs rename to src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs index 0616b4098f..3cc5e061a5 100644 --- a/src/Umbraco.Tests/Cache/CacheRefresherComponentTests.cs +++ b/src/Umbraco.Tests/Cache/DistributedCacheBinderTests.cs @@ -1,21 +1,23 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Threading; +using Moq; using NUnit.Framework; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; using Umbraco.Web.Cache; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; namespace Umbraco.Tests.Cache { [TestFixture] [UmbracoTest(WithApplication = true)] - public class CacheRefresherEventHandlerTests : UmbracoTestBase + public class DistributedCacheBinderTests : UmbracoTestBase { [Test] public void Can_Find_All_Event_Handlers() @@ -114,7 +116,7 @@ namespace Umbraco.Tests.Cache var ok = true; foreach (var definition in definitions) { - var found = CacheRefresherComponent.FindHandler(definition); + var found = DistributedCacheBinder.FindHandler(definition); if (found == null) { Console.WriteLine("Couldn't find method for " + definition.EventName + " on " + definition.Sender.GetType()); @@ -123,5 +125,35 @@ namespace Umbraco.Tests.Cache } Assert.IsTrue(ok, "see log for details"); } + + [Test] + public void CanHandleEvent() + { + // refreshers.HandleEvents wants a UmbracoContext + // which wants an HttpContext, which we build using a SimpleWorkerRequest + // which requires these to be non-null + var domain = Thread.GetDomain(); + if (domain.GetData(".appPath") == null) + domain.SetData(".appPath", ""); + if (domain.GetData(".appVPath") == null) + domain.SetData(".appVPath", ""); + + // refreshers.HandleEvents wants a UmbracoContext + // which wants these + Container.RegisterSingleton(_ => Mock.Of()); + Container.RegisterCollectionBuilder(); + + // create some event definitions + var definitions = new IEventDefinition[] + { + // works because that event definition maps to an empty handler + new EventDefinition>(null, Current.Services.ContentTypeService, new SaveEventArgs(Enumerable.Empty()), "Saved"), + + }; + + // just assert it does not throw + var refreshers = new DistributedCacheBinder(null, null); + refreshers.HandleEvents(definitions); + } } } diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index af188c6a09..2aee582acc 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -2,19 +2,17 @@ using System.Collections.Generic; using System.Linq; using LightInject; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Sync; -using Umbraco.Tests.Cache.DistributedCache; using Umbraco.Tests.Services; using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.TestHelpers.Stubs; using Umbraco.Tests.Testing; using Umbraco.Web.Cache; using static Umbraco.Tests.Cache.DistributedCache.DistributedCacheTests; @@ -34,8 +32,8 @@ namespace Umbraco.Tests.Integration { base.SetUp(); - _h1 = new CacheRefresherComponent(true); - _h1.Initialize(new DistributedCache()); + _h1 = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _h1.BindEvents(true); _events = new List(); @@ -76,7 +74,7 @@ namespace Umbraco.Tests.Integration { base.TearDown(); - _h1?.Unbind(); + _h1?.UnbindEvents(); // clear ALL events @@ -86,7 +84,7 @@ namespace Umbraco.Tests.Integration ContentCacheRefresher.CacheUpdated -= ContentCacheUpdated; } - private CacheRefresherComponent _h1; + private DistributedCacheBinder _h1; private IList _events; private int _msgCount; private IContentType _contentType; diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index cf3285cd7e..211bdc3cdb 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Events; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence.Repositories; @@ -36,7 +37,7 @@ namespace Umbraco.Tests.Scoping [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true)] public class ScopedNuCacheTests : TestWithDatabaseBase { - private CacheRefresherComponent _cacheRefresher; + private DistributedCacheBinder _distributedCacheBinder; protected override void Compose() { @@ -56,8 +57,8 @@ namespace Umbraco.Tests.Scoping { base.TearDown(); - _cacheRefresher?.Unbind(); - _cacheRefresher = null; + _distributedCacheBinder?.UnbindEvents(); + _distributedCacheBinder = null; _onPublishedAssertAction = null; ContentService.Published -= OnPublishedAssert; @@ -129,8 +130,8 @@ namespace Umbraco.Tests.Scoping var umbracoContext = GetUmbracoContextNu("http://example.com/", setSingleton: true); // wire cache refresher - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); // create document type, document var contentType = new ContentType(-1) { Alias = "CustomDocument", Name = "Custom Document" }; diff --git a/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs index 48bbdb1e22..9ace5860e1 100644 --- a/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedRepositoryTests.cs @@ -13,6 +13,7 @@ using Umbraco.Web.Cache; using LightInject; using Moq; using Umbraco.Core.Events; +using Umbraco.Core.Logging; using Umbraco.Core.Sync; namespace Umbraco.Tests.Scoping @@ -21,7 +22,7 @@ namespace Umbraco.Tests.Scoping [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, WithApplication = true)] public class ScopedRepositoryTests : TestWithDatabaseBase { - private CacheRefresherComponent _cacheRefresher; + private DistributedCacheBinder _distributedCacheBinder; protected override void Compose() { @@ -52,8 +53,8 @@ namespace Umbraco.Tests.Scoping [TearDown] public void Teardown() { - _cacheRefresher?.Unbind(); - _cacheRefresher = null; + _distributedCacheBinder?.UnbindEvents(); + _distributedCacheBinder = null; } [TestCase(true)] @@ -76,8 +77,8 @@ namespace Umbraco.Tests.Scoping // get user again - else we'd modify the one that's in the cache user = service.GetUserById(user.Id); - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); Assert.IsNull(scopeProvider.AmbientScope); using (var scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) @@ -157,8 +158,8 @@ namespace Umbraco.Tests.Scoping Assert.AreEqual(lang.Id, globalCached.Id); Assert.AreEqual("fr-FR", globalCached.IsoCode); - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); Assert.IsNull(scopeProvider.AmbientScope); using (var scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) @@ -249,8 +250,8 @@ namespace Umbraco.Tests.Scoping Assert.AreEqual(item.Id, globalCached.Id); Assert.AreEqual("item-key", globalCached.ItemKey); - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); Assert.IsNull(scopeProvider.AmbientScope); using (var scope = scopeProvider.CreateScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) diff --git a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs index 5f4e653735..1afbf5cb12 100644 --- a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs @@ -7,6 +7,7 @@ using LightInject; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Events; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; @@ -23,7 +24,7 @@ namespace Umbraco.Tests.Scoping [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, PublishedRepositoryEvents = true)] public class ScopedXmlTests : TestWithDatabaseBase { - private CacheRefresherComponent _cacheRefresher; + private DistributedCacheBinder _distributedCacheBinder; protected override void Compose() { @@ -42,8 +43,8 @@ namespace Umbraco.Tests.Scoping [TearDown] public void Teardown() { - _cacheRefresher?.Unbind(); - _cacheRefresher = null; + _distributedCacheBinder?.UnbindEvents(); + _distributedCacheBinder = null; _onPublishedAssertAction = null; ContentService.Published -= OnPublishedAssert; @@ -90,8 +91,8 @@ namespace Umbraco.Tests.Scoping var item = new Content("name", -1, contentType); // wire cache refresher - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); // check xml in context = "before" var xml = XmlInContext; @@ -209,8 +210,8 @@ namespace Umbraco.Tests.Scoping Current.Services.ContentTypeService.Save(contentType); // wire cache refresher - _cacheRefresher = new CacheRefresherComponent(true); - _cacheRefresher.Initialize(new DistributedCache()); + _distributedCacheBinder = new DistributedCacheBinder(new DistributedCache(), Mock.Of()); + _distributedCacheBinder.BindEvents(true); // check xml in context = "before" var xml = XmlInContext; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7d1753a6b2..d58cf2a5b0 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -111,7 +111,7 @@ - + diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs new file mode 100644 index 0000000000..f7171a840a --- /dev/null +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Cache +{ + /// + /// Default implementation. + /// + public partial class DistributedCacheBinder : IDistributedCacheBinder + { + private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); + private readonly DistributedCache _distributedCache; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public DistributedCacheBinder(DistributedCache distributedCache, ILogger logger) + { + _distributedCache = distributedCache; + _logger = logger; + } + + // internal for tests + internal static MethodInfo FindHandler(IEventDefinition eventDefinition) + { + var name = eventDefinition.Sender.GetType().Name + "_" + eventDefinition.EventName; + + return FoundHandlers.GetOrAdd(name, n => CandidateHandlers.Value.FirstOrDefault(x => x.Name == n)); + } + + private static readonly Lazy CandidateHandlers = new Lazy(() => + { + var underscore = new[] { '_' }; + + return typeof(DistributedCacheBinder) + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Select(x => + { + if (x.Name.Contains("_") == false) return null; + + var parts = x.Name.Split(underscore, StringSplitOptions.RemoveEmptyEntries).Length; + if (parts != 2) return null; + + var parameters = x.GetParameters(); + if (parameters.Length != 2) return null; + if (typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType) == false) return null; + return x; + }) + .WhereNotNull() + .ToArray(); + }); + + /// + public void HandleEvents(IEnumerable events) + { + // ensure we run with an UmbracoContext, because this may run in a background task, + // yet developers may be using the 'current' UmbracoContext in the event handlers + using (UmbracoContext.EnsureContext()) + { + foreach (var e in events) + { + var handler = FindHandler(e); + if (handler == null) + { + // fixme - should this be fatal (ie, an exception)? + var name = e.Sender.GetType().Name + "_" + e.EventName; + _logger.Warn("Dropping event {EventName} because no corresponding handler was found.", name); + continue; + } + + handler.Invoke(this, new[] { e.Sender, e.Args }); + } + } + } + } +} diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinderComponent.cs b/src/Umbraco.Web/Cache/DistributedCacheBinderComponent.cs new file mode 100644 index 0000000000..57b013a97c --- /dev/null +++ b/src/Umbraco.Web/Cache/DistributedCacheBinderComponent.cs @@ -0,0 +1,24 @@ +using Umbraco.Core; +using Umbraco.Core.Components; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Cache +{ + /// + /// Installs listeners on service events in order to refresh our caches. + /// + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + [RequiredComponent(typeof(IUmbracoCoreComponent))] // runs before every other IUmbracoCoreComponent! + public class DistributedCacheBinderComponent : UmbracoComponentBase, IUmbracoCoreComponent + { + public override void Compose(Composition composition) + { + composition.Container.RegisterSingleton(); + } + + public void Initialize(IDistributedCacheBinder distributedCacheBinder) + { + distributedCacheBinder.BindEvents(); + } + } +} diff --git a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs similarity index 86% rename from src/Umbraco.Web/Cache/CacheRefresherComponent.cs rename to src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs index a020b0d76f..81b133b9ef 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherComponent.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs @@ -1,56 +1,50 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; -using Umbraco.Core; +using System.Linq; using Umbraco.Core.Events; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; -using System.Linq; -using System.Reflection; -using System.Web; -using System.Web.Hosting; -using Umbraco.Core.Components; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services.Changes; using Umbraco.Core.Services.Implement; -using Umbraco.Web.Composing; -using Umbraco.Web.Security; using Umbraco.Web.Services; -using LightInject; -using ApplicationTree = Umbraco.Core.Models.ApplicationTree; namespace Umbraco.Web.Cache { /// - /// Installs listeners on service events in order to refresh our caches. + /// Default implementation. /// - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - [RequiredComponent(typeof(IUmbracoCoreComponent))] // runs before every other IUmbracoCoreComponent! - public class CacheRefresherComponent : UmbracoComponentBase, IUmbracoCoreComponent + public partial class DistributedCacheBinder { - private static readonly ConcurrentDictionary FoundHandlers = new ConcurrentDictionary(); - private DistributedCache _distributedCache; private List _unbinders; - public CacheRefresherComponent() - { } + private void Bind(Action binder, Action unbinder) + { + // bind now + binder(); - // for tests - public CacheRefresherComponent(bool supportUnbinding) + // and register unbinder for later, if needed + _unbinders?.Add(unbinder); + } + + /// + public void UnbindEvents() + { + if (_unbinders == null) + throw new NotSupportedException(); + foreach (var unbinder in _unbinders) + unbinder(); + _unbinders = null; + } + + /// + public void BindEvents(bool supportUnbinding = false) { if (supportUnbinding) _unbinders = new List(); - } - public void Initialize(DistributedCache distributedCache) - { - Current.Logger.Info("Initializing Umbraco internal event handlers for cache refreshing."); - - _distributedCache = distributedCache; + _logger.Info("Initializing Umbraco internal event handlers for cache refreshing."); // bind to application tree events Bind(() => ApplicationTreeService.Deleted += ApplicationTreeService_Deleted, @@ -170,74 +164,6 @@ namespace Umbraco.Web.Cache () => RelationService.DeletedRelationType -= RelationService_DeletedRelationType); } - #region Events binding and handling - - private void Bind(Action binder, Action unbinder) - { - // bind now - binder(); - - // and register unbinder for later, if needed - _unbinders?.Add(unbinder); - } - - // for tests - internal void Unbind() - { - if (_unbinders == null) - throw new NotSupportedException(); - foreach (var unbinder in _unbinders) - unbinder(); - _unbinders = null; - } - - internal static MethodInfo FindHandler(IEventDefinition eventDefinition) - { - var name = eventDefinition.Sender.GetType().Name + "_" + eventDefinition.EventName; - - return FoundHandlers.GetOrAdd(name, n => CandidateHandlers.Value.FirstOrDefault(x => x.Name == n)); - } - - private static readonly Lazy CandidateHandlers = new Lazy(() => - { - var underscore = new[] { '_' }; - - return typeof(CacheRefresherComponent) - .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Select(x => - { - if (x.Name.Contains("_") == false) return null; - - var parts = x.Name.Split(underscore, StringSplitOptions.RemoveEmptyEntries).Length; - if (parts != 2) return null; - - var parameters = x.GetParameters(); - if (parameters.Length != 2) return null; - if (typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType) == false) return null; - return x; - }) - .WhereNotNull() - .ToArray(); - }); - - internal static void HandleEvents(IEnumerable events) - { - // ensure we run with an UmbracoContext, because this may run in a background task, - // yet developers may be using the 'current' UmbracoContext in the event handlers - using (UmbracoContext.EnsureContext()) - { - foreach (var e in events) - { - var handler = FindHandler(e); - if (handler == null) continue; - - handler.Invoke(null, new[] { e.Sender, e.Args }); - } - } - } - - #endregion - #region PublicAccessService private void PublicAccessService_Saved(IPublicAccessService sender, SaveEventArgs e) @@ -264,7 +190,8 @@ namespace Umbraco.Web.Cache /// case then we need to clear all user permissions cache. /// private void ContentService_Copied(IContentService sender, CopyEventArgs e) - { } + { + } /// /// Handles cache refreshing for when content is saved (not published) @@ -276,7 +203,8 @@ namespace Umbraco.Web.Cache /// stay up-to-date for unpublished content. /// private void ContentService_Saved(IContentService sender, SaveEventArgs e) - { } + { + } private void ContentService_Changed(IContentService sender, TreeChange.EventArgs args) { @@ -324,12 +252,12 @@ namespace Umbraco.Web.Cache #region Application event handlers - private void SectionService_New(Section sender, EventArgs e) + private void SectionService_New(ISectionService sender, EventArgs e) { _distributedCache.RefreshAllApplicationCache(); } - private void SectionService_Deleted(Section sender, EventArgs e) + private void SectionService_Deleted(ISectionService sender, EventArgs e) { _distributedCache.RefreshAllApplicationCache(); } @@ -439,7 +367,8 @@ namespace Umbraco.Web.Cache #region UserService - static void UserService_UserGroupPermissionsAssigned(IUserService sender, SaveEventArgs e) + // fixme STATIC?? + private void UserService_UserGroupPermissionsAssigned(IUserService sender, SaveEventArgs e) { //TODO: Not sure if we need this yet depends if we start caching permissions //var groupIds = e.SavedEntities.Select(x => x.UserGroupId).Distinct(); @@ -568,6 +497,7 @@ namespace Umbraco.Web.Cache _distributedCache.RemoveMemberGroupCache(m.Id); } } + #endregion #region RelationType diff --git a/src/Umbraco.Web/Cache/IDistributedCacheBinder.cs b/src/Umbraco.Web/Cache/IDistributedCacheBinder.cs new file mode 100644 index 0000000000..e4237fea94 --- /dev/null +++ b/src/Umbraco.Web/Cache/IDistributedCacheBinder.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Web.Cache +{ + /// + /// Binds events to the distributed cache. + /// + /// + /// Use to bind actual events, eg , to + /// the distributed cache, so that the proper refresh operations are executed when these events trigger. + /// Use to handle events that have not actually triggered, but have + /// been queued, so that the proper refresh operations are also executed. + /// + public interface IDistributedCacheBinder + { + /// + /// Handles events from definitions. + /// + void HandleEvents(IEnumerable events); + + /// + /// Binds actual events to the distributed cache. + /// + /// A value indicating whether to support unbinding the events. + void BindEvents(bool enableUnbinding = false); + + /// + /// Unbinds bounded events. + /// + void UnbindEvents(); + } +} diff --git a/src/Umbraco.Web/Services/SectionService.cs b/src/Umbraco.Web/Services/SectionService.cs index ff8279a411..5d013d7e79 100644 --- a/src/Umbraco.Web/Services/SectionService.cs +++ b/src/Umbraco.Web/Services/SectionService.cs @@ -200,7 +200,7 @@ namespace Umbraco.Web.Services }, true); //raise event - OnNew(new Section(name, alias, sortOrder), new EventArgs()); + OnNew(this /*new Section(name, alias, sortOrder)*/, new EventArgs()); } } @@ -235,7 +235,7 @@ namespace Umbraco.Web.Services }, true); //raise event - OnDeleted(section, new EventArgs()); + OnDeleted(this, new EventArgs()); } } @@ -262,8 +262,8 @@ namespace Umbraco.Web.Services return tmp; } - internal static event TypedEventHandler Deleted; - private static void OnDeleted(Section app, EventArgs args) + internal static event TypedEventHandler Deleted; + private static void OnDeleted(ISectionService app, EventArgs args) { if (Deleted != null) { @@ -271,8 +271,8 @@ namespace Umbraco.Web.Services } } - internal static event TypedEventHandler New; - private static void OnNew(Section app, EventArgs args) + internal static event TypedEventHandler New; + private static void OnNew(ISectionService app, EventArgs args) { if (New != null) { diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6089e68325..17c2a54d81 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -108,7 +108,10 @@ + + + @@ -652,7 +655,7 @@ - + From 17d818b6049c79f2d0403c05bbe7bb13154c79b6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 2 Jan 2019 15:29:22 +0100 Subject: [PATCH 30/33] Make Current.Container public for Deploy, wont last --- src/Umbraco.Core/Composing/Current.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index e3bce53a3c..9515398236 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -38,7 +38,7 @@ namespace Umbraco.Core.Composing /// /// Gets or sets the DI container. /// - internal static IServiceContainer Container + public static IServiceContainer Container { get { From 2dfbb1d555ef717ca3bcfb76ff8c843933f8fd17 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 2 Jan 2019 20:57:18 +0100 Subject: [PATCH 31/33] Don't attempt to apply a filter if none is submitted --- src/Umbraco.Web/Editors/EntityController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 5e73f4140e..993489855f 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -479,7 +479,9 @@ namespace Umbraco.Web.Editors if (objectType.HasValue) { var entities = Services.EntityService.GetPagedChildren(id, objectType.Value, pageNumber - 1, pageSize, out var totalRecords, - SqlContext.Query().Where(x => x.Name.Contains(filter)), + filter.IsNullOrWhiteSpace() + ? null + : SqlContext.Query().Where(x => x.Name.Contains(filter)), Ordering.By(orderBy, orderDirection)); if (totalRecords == 0) From 525b229b42edf44d4fdff834354721ca5e840d6a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 2 Jan 2019 15:20:25 +0100 Subject: [PATCH 32/33] Fix content creation from list views --- .../listview/listview.controller.js | 23 +++++++++++++------ .../propertyeditors/listview/listview.html | 16 ++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 1d8cdfa03f..96e7ccdbfd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -701,16 +701,23 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs } getContentTypesCallback(id).then(function (listViewAllowedTypes) { - $scope.listViewAllowedTypes = listViewAllowedTypes; + $scope.listViewAllowedTypes = listViewAllowedTypes; var blueprints = false; _.each(listViewAllowedTypes, function (allowedType) { if (_.isEmpty(allowedType.blueprints)) { - // this helps the view understand that there are no blueprints available - allowedType.blueprints = null; + // this helps the view understand that there are no blueprints available + allowedType.blueprints = null; } else { - blueprints = true; + blueprints = true; + // turn the content type blueprints object into an array of sortable objects for the view + allowedType.blueprints = _.map(_.pairs(allowedType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); } }); @@ -776,17 +783,19 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs } } - function createBlank(entityType, docTypeAlias) { $location .path("/" + entityType + "/" + entityType + "/edit/" + $scope.contentId) - .search("doctype=" + docTypeAlias + "&create=true"); + .search("doctype", docTypeAlias) + .search("create", "true"); } function createFromBlueprint(entityType, docTypeAlias, blueprintId) { $location .path("/" + entityType + "/" + entityType + "/edit/" + $scope.contentId) - .search("doctype=" + docTypeAlias + "&create=true&blueprintId=" + blueprintId); + .search("doctype", docTypeAlias) + .search("create", "true") + .search("blueprintId", blueprintId); } $scope.createBlank = createBlank; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 44636d7033..77576cac6a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -27,10 +27,10 @@ - - + + - {{::value}} + {{::blueprint.name}} @@ -48,9 +48,9 @@ {{::contentType.name}} (blank) - +    - {{::value}} + {{::blueprint.name}} @@ -58,11 +58,11 @@
    -
  • - +
  • + - {{value}} + {{blueprint.name}}
  • From 4b60d8bca614ac9a0f8466e84023cdd09810cc90 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 2 Jan 2019 14:58:18 +0100 Subject: [PATCH 33/33] Fix the content template selector in the content tree --- .../src/views/content/content.create.controller.js | 13 ++++++++++--- .../src/views/content/create.html | 8 ++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 6b1021b663..9fbf342435 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -42,14 +42,21 @@ function contentCreateController($scope, } function createOrSelectBlueprintIfAny(docType) { - var blueprintIds = _.keys(docType.blueprints || {}); + // map the blueprints into a collection that's sortable in the view + var blueprints = _.map(_.pairs(docType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); $scope.docType = docType; - if (blueprintIds.length) { + if (blueprints.length) { if (blueprintConfig.skipSelect) { - createFromBlueprint(blueprintIds[0]); + createFromBlueprint(blueprints[0].id); } else { $scope.selectContentType = false; $scope.selectBlueprint = true; + $scope.selectableBlueprints = blueprints; } } else { createBlank(docType); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index e76d17caa8..94299f6a54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -25,13 +25,13 @@
-