diff --git a/src/Umbraco.Core/ActionsResolver.cs b/src/Umbraco.Core/ActionsResolver.cs index 206182c6f2..ff2eaa8553 100644 --- a/src/Umbraco.Core/ActionsResolver.cs +++ b/src/Umbraco.Core/ActionsResolver.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Umbraco.Core.Logging; @@ -34,7 +35,31 @@ namespace Umbraco.Core { return Values; } - } + } + + /// + /// This method will return a list of IAction's based on a string (letter) list. Each character in the list may represent + /// an IAction. This will associate any found IActions based on the Letter property of the IAction with the character being referenced. + /// + /// + /// returns a list of actions that have an associated letter found in the action string list + public IEnumerable FromActionSymbols(IEnumerable actions) + { + var allActions = Actions.ToArray(); + return actions + .Select(c => allActions.FirstOrDefault(a => a.Letter.ToString(CultureInfo.InvariantCulture) == c)) + .WhereNotNull() + .ToArray(); + } + + /// + /// Returns the string (letter) representation of the actions that make up the actions collection + /// + /// + public IEnumerable ToActionSymbols(IEnumerable actions) + { + return actions.Select(x => x.Letter.ToString(CultureInfo.InvariantCulture)).ToArray(); + } /// /// Gets an Action if it exists. diff --git a/src/Umbraco.Core/Models/UmbracoEntity.cs b/src/Umbraco.Core/Models/UmbracoEntity.cs index 48752468d5..b789698704 100644 --- a/src/Umbraco.Core/Models/UmbracoEntity.cs +++ b/src/Umbraco.Core/Models/UmbracoEntity.cs @@ -51,6 +51,8 @@ namespace Umbraco.Core.Models private string _contentTypeIcon; private string _contentTypeThumbnail; + public static readonly UmbracoEntity Root = new UmbracoEntity(false) {Path = "-1", Name = "root", HasChildren = true}; + public UmbracoEntity() { AdditionalData = new Dictionary(); diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index b842fb8f54..c9749348a8 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -6,6 +6,7 @@ using System.Net; using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.IO; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -13,7 +14,12 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Models { public static class UserExtensions - { + { + public static IEnumerable GetPermissions(this IUser user, string path, IUserService userService) + { + return userService.GetPermissionsForPath(user, path).GetAllPermissions(); + } + public static bool HasSectionAccess(this IUser user, string app) { var apps = user.AllowedSections; @@ -150,6 +156,19 @@ namespace Umbraco.Core.Models return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia); } + internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId) + { + switch (recycleBinId) + { + case Constants.System.RecycleBinMedia: + return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId); + case Constants.System.RecycleBinContent: + return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId); + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId) { if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); @@ -162,16 +181,73 @@ namespace Umbraco.Core.Models if (startNodeIds.Contains(Constants.System.Root)) return true; - var formattedPath = "," + path + ","; + var formattedPath = string.Concat(",", path, ","); // only users with root access have access to the recycle bin, // if the above check didn't pass then access is denied - if (formattedPath.Contains("," + recycleBinId + ",")) + if (formattedPath.Contains(string.Concat(",", recycleBinId, ","))) return false; // check for a start node in the path - return startNodeIds.Any(x => formattedPath.Contains("," + x + ",")); + return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ","))); } + + internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess) + { + switch (recycleBinId) + { + case Constants.System.RecycleBinMedia: + return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess); + case Constants.System.RecycleBinContent: + return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess); + default: + throw new NotSupportedException("Path access is only determined on content or media"); + } + } + + internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", "path"); + + hasPathAccess = false; + + // check for no access + if (startNodeIds.Length == 0) + return false; + + // check for root access + if (startNodeIds.Contains(Constants.System.Root)) + { + hasPathAccess = true; + return true; + } + + //is it self? + var self = startNodePaths.Any(x => x == path); + if (self) + { + hasPathAccess = true; + return true; + } + + //is it ancestor? + var ancestor = startNodePaths.Any(x => x.StartsWith(path)); + if (ancestor) + { + hasPathAccess = false; + return true; + } + + //is it descendant? + var descendant = startNodePaths.Any(x => path.StartsWith(x)); + if (descendant) + { + hasPathAccess = true; + return true; + } + + return false; + } /// /// Determines whether this user is an admin. @@ -191,7 +267,7 @@ namespace Umbraco.Core.Models { const string cacheKey = "AllContentStartNodes"; //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); + var valuesInUserCache = FromUserCache(user, cacheKey); if (valuesInUserCache != null) return valuesInUserCache; var gsn = user.Groups.Where(x => x.StartContentId.HasValue).Select(x => x.StartContentId.Value).Distinct().ToArray(); @@ -206,7 +282,7 @@ namespace Umbraco.Core.Models { const string cacheKey = "AllMediaStartNodes"; //try to look them up from cache so we don't recalculate - var valuesInUserCache = FromUserCache(user, cacheKey); + var valuesInUserCache = FromUserCache(user, cacheKey); if (valuesInUserCache != null) return valuesInUserCache; var gsn = user.Groups.Where(x => x.StartMediaId.HasValue).Select(x => x.StartMediaId.Value).Distinct().ToArray(); @@ -214,9 +290,36 @@ namespace Umbraco.Core.Models var vals = CombineStartNodes(UmbracoObjectTypes.Media, gsn, usn, entityService); ToUserCache(user, cacheKey, vals); return vals; + } + + public static string[] GetMediaStartNodePaths(this IUser user, IEntityService entityService) + { + const string cacheKey = "MediaStartNodePaths"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var startNodeIds = user.CalculateMediaStartNodeIds(entityService); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Media, startNodeIds).Select(x => x.Path).ToArray(); + ToUserCache(user, cacheKey, vals); + return vals; + } + + public static string[] GetContentStartNodePaths(this IUser user, IEntityService entityService) + { + const string cacheKey = "ContentStartNodePaths"; + //try to look them up from cache so we don't recalculate + var valuesInUserCache = FromUserCache(user, cacheKey); + if (valuesInUserCache != null) return valuesInUserCache; + + var startNodeIds = user.CalculateContentStartNodeIds(entityService); + var vals = entityService.GetAllPaths(UmbracoObjectTypes.Document, startNodeIds).Select(x => x.Path).ToArray(); + ToUserCache(user, cacheKey, vals); + return vals; } - private static int[] FromUserCache(IUser user, string cacheKey) + private static T FromUserCache(IUser user, string cacheKey) + where T: class { var entityUser = user as User; if (entityUser == null) return null; @@ -225,12 +328,13 @@ namespace Umbraco.Core.Models { object allContentStartNodes; return entityUser.AdditionalData.TryGetValue(cacheKey, out allContentStartNodes) - ? allContentStartNodes as int[] + ? allContentStartNodes as T : null; } } - private static void ToUserCache(IUser user, string cacheKey, int[] vals) + private static void ToUserCache(IUser user, string cacheKey, T vals) + where T: class { var entityUser = user as User; if (entityUser == null) return; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 1a652a787c..6ed1a21306 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -31,7 +31,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //var showheader = (attrs.showheader !== 'false'); var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : ""; var template = '
  • '; - template += '
    ' + + template += '
    ' + '
    ' + ' {{tree.name}}
    ' + '' + @@ -310,6 +310,25 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } + /** Returns the css classses assigned to the node (div element) */ + scope.getNodeCssClass = function (node) { + if (!node) { + return ''; + } + + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + + var css = []; + if (node.cssClasses) { + _.each(node.cssClasses, function (c) { + css.push(c); + }); + } + + return css.join(" "); + }; + scope.selectEnabledNodeClass = function (node) { return node ? node.selected ? @@ -383,6 +402,12 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat defined on the tree */ scope.select = function (n, ev) { + + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + //on tree select we need to remove the current node - // whoever handles this will need to make sure the correct node is selected //reset current node selection diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index b32942791c..25c1becc87 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -41,7 +41,7 @@ angular.module("umbraco.directives") //'' + ' ' + '' + - '' + + '' + //NOTE: These are the 'option' elipses '' + '
    ' + @@ -112,6 +112,10 @@ angular.module("umbraco.directives") if (!node) { return ''; } + + //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + var css = []; if (node.cssClasses) { _.each(node.cssClasses, function(c) { @@ -121,6 +125,7 @@ angular.module("umbraco.directives") if (node.selected) { css.push("umb-tree-node-checked"); } + return css.join(" "); }; @@ -158,6 +163,11 @@ angular.module("umbraco.directives") return; } + if (n.metaData && n.metaData.noAccess === true) { + ev.preventDefault(); + return; + } + emitEvent("treeNodeSelect", { element: element, tree: scope.tree, node: n, event: ev }); ev.preventDefault(); }; @@ -229,6 +239,12 @@ angular.module("umbraco.directives") setupNodeDom(scope.node, scope.tree); + // load the children if the current user don't have access to the node + // it is used to auto expand the tree to the start nodes the user has access to + if(scope.node.hasChildren && scope.node.metaData.noAccess) { + scope.loadChildren(scope.node); + } + var template = '
    '; var newElement = angular.element(template); $compile(newElement)(scope); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js index c692fda7ba..8738c1011e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/search.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/search.service.js @@ -44,7 +44,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) { _.each(data, function (item) { - configureMemberResult(item); + searchResultFormatter.configureMemberResult(item); }); return data; }); @@ -69,7 +69,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) { _.each(data, function (item) { - configureContentResult(item); + searchResultFormatter.configureContentResult(item); }); return data; }); @@ -94,7 +94,7 @@ angular.module('umbraco.services') return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) { _.each(data, function (item) { - configureMediaResult(item); + searchResultFormatter.configureMediaResult(item); }); return data; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 5469c12c30..888a067a66 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -44,52 +44,69 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!parentNode.section) { parentNode.section = section; } + + if (parentNode.metaData && parentNode.metaData.noAccess === true) { + if (!parentNode.cssClasses) { + parentNode.cssClasses = []; + } + parentNode.cssClasses.push("no-access"); + } + //create a method outside of the loop to return the parent - otherwise jshint blows up var funcParent = function() { return parentNode; }; for (var i = 0; i < treeNodes.length; i++) { - treeNodes[i].level = childLevel; + var treeNode = treeNodes[i]; + + treeNode.level = childLevel; //create a function to get the parent node, we could assign the parent node but // then we cannot serialize this entity because we have a cyclical reference. // Instead we just make a function to return the parentNode. - treeNodes[i].parent = funcParent; + treeNode.parent = funcParent; //set the section for each tree node - this allows us to reference this easily when accessing tree nodes - treeNodes[i].section = section; + treeNode.section = section; //if there is not route path specified, then set it automatically, //if this is a tree root node then we want to route to the section's dashboard - if (!treeNodes[i].routePath) { + if (!treeNode.routePath) { - if (treeNodes[i].metaData && treeNodes[i].metaData["treeAlias"]) { + if (treeNode.metaData && treeNode.metaData["treeAlias"]) { //this is a root node - treeNodes[i].routePath = section; + treeNode.routePath = section; } else { - var treeAlias = this.getTreeAlias(treeNodes[i]); - treeNodes[i].routePath = section + "/" + treeAlias + "/edit/" + treeNodes[i].id; + var treeAlias = this.getTreeAlias(treeNode); + treeNode.routePath = section + "/" + treeAlias + "/edit/" + treeNode.id; } } - + //now, format the icon data - if (treeNodes[i].iconIsClass === undefined || treeNodes[i].iconIsClass) { - var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNodes[i]); - treeNodes[i].cssClass = standardCssClass + " " + converted; + if (treeNode.iconIsClass === undefined || treeNode.iconIsClass) { + var converted = iconHelper.convertFromLegacyTreeNodeIcon(treeNode); + treeNode.cssClass = standardCssClass + " " + converted; if (converted.startsWith('.')) { //its legacy so add some width/height - treeNodes[i].style = "height:16px;width:16px;"; + treeNode.style = "height:16px;width:16px;"; } else { - treeNodes[i].style = ""; + treeNode.style = ""; } } else { - treeNodes[i].style = "background-image: url('" + treeNodes[i].iconFilePath + "');"; + treeNode.style = "background-image: url('" + treeNode.iconFilePath + "');"; //we need an 'icon-' class in there for certain styles to work so if it is image based we'll add this - treeNodes[i].cssClass = standardCssClass + " legacy-custom-file"; + treeNode.cssClass = standardCssClass + " legacy-custom-file"; + } + + if (treeNode.metaData && treeNode.metaData.noAccess === true) { + if (!treeNode.cssClasses) { + treeNode.cssClasses = []; + } + treeNode.cssClasses.push("no-access"); } } }, @@ -375,9 +392,10 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS } for (var i = 0; i < treeNode.children.length; i++) { - if (treeNode.children[i].children && angular.isArray(treeNode.children[i].children) && treeNode.children[i].children.length > 0) { + var child = treeNode.children[i]; + if (child.children && angular.isArray(child.children) && child.children.length > 0) { //recurse - found = this.getDescendantNode(treeNode.children[i], id); + found = this.getDescendantNode(child, id); if (found) { return found; } diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index 5de27de9f3..c859fae991 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -377,6 +377,13 @@ div.locked:before{ bottom: 0; } +.umb-tree li div.no-access .umb-tree-icon, +.umb-tree li div.no-access .root-link, +.umb-tree li div.no-access .umb-tree-item__label { + color: @gray-7; + cursor: not-allowed; +} + // Tree context menu // ------------------------- .umb-actions { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index 6820e81f31..b18aa96588 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -169,10 +169,10 @@ angular.module("umbraco") $scope.showPasswordFields = !$scope.showPasswordFields; } - function clearPasswordFields() { + function clearPasswordFields() { $scope.changePasswordModel.value.oldPassword = ""; $scope.changePasswordModel.value.newPassword = ""; $scope.changePasswordModel.value.confirm = ""; } - + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index 9940aebe64..fa2d831eac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -18,6 +18,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", results: [], selectedSearchResults: [] } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); var node = dialogOptions.currentNode; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index ce81a60a38..984c147ee4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", - function ($scope, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { + function ($scope, userService, eventsService, contentResource, navigationService, appState, treeService, localizationService, notificationsService) { var dialogOptions = $scope.dialogOptions; var searchText = "Search..."; @@ -15,7 +15,13 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", showSearch: false, results: [], selectedSearchResults: [] - } + } + $scope.treeModel = { + hideHeader: false + } + userService.getCurrentUser().then(function (userData) { + $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; + }); var node = dialogOptions.currentNode; 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 c19ec72dbd..ca7714c284 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -48,7 +48,7 @@
    - 0 && userData.startMediaIds.indexOf(-1) == -1; + }); + function nodeSelectHandler(ev, args) { if(args && args.event) { diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 52be1832f0..d04d66f706 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -27,7 +27,7 @@
    (user); + var result = Mapper.Map(user); + return result; } /// diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 356a09a556..6c40643766 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -249,7 +249,7 @@ namespace Umbraco.Web.Models.Mapping user.CalculateMediaStartNodeIds(applicationContext.Services.EntityService), applicationContext.Services.TextService, applicationContext.Services.EntityService, - UmbracoObjectTypes.Document, + UmbracoObjectTypes.Media, "media/mediaRoot"))) .ForMember( detail => detail.StartContentIds, diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9b3a6e82f8..ea644f5dc8 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -41,22 +41,8 @@ namespace Umbraco.Web.Trees [SearchableTree("searchResultFormatter", "configureContentResult")] public class ContentTreeController : ContentTreeControllerBase, ISearchableTree { - private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); - - protected override TreeNode CreateRootNode(FormDataCollection queryStrings) - { - var node = base.CreateRootNode(queryStrings); - - // if the user's start node is not default, then ensure the root doesn't have a menu - if (UserStartNodes.Contains(Constants.System.Root) == false) - { - node.MenuUrl = ""; - } - node.Name = ui.Text("sections", Constants.Trees.Content); - return node; - } - + protected override int RecycleBinId { get { return Constants.System.RecycleBinContent; } @@ -123,14 +109,17 @@ namespace Umbraco.Web.Trees } protected override MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings) - { + { if (id == Constants.System.Root.ToInvariantString()) { var menu = new MenuItemCollection(); - // if the user's start node is not the root then ensure the root menu is empty/doesn't exist + // if the user's start node is not the root then the only menu item to display is refresh if (UserStartNodes.Contains(Constants.System.Root) == false) { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); return menu; } @@ -174,6 +163,16 @@ namespace Umbraco.Web.Trees { throw new HttpResponseException(HttpStatusCode.NotFound); } + + //if the user has no path access for this node, all they can do is refresh + if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false) + { + var menu = new MenuItemCollection(); + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); + return menu; + } var nodeMenu = GetAllNodeMenuItems(item); var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); @@ -210,17 +209,7 @@ namespace Umbraco.Web.Trees protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); - if (entity == null) - { - return false; - } - - var content = Services.ContentService.GetById(entity.Id); - if (content == null) - { - return false; - } - return Security.CurrentUser.HasPathAccess(content, Services.EntityService); + return HasPathAccess(entity, queryStrings); } /// diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index fa18e703bd..8fabc1677e 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Net; using System.Net.Http; @@ -52,8 +54,48 @@ namespace Umbraco.Web.Trees } #endregion + + /// + /// Ensure the noAccess metadata is applied for the root node if in dialog mode and the user doesn't have path access to it + /// + /// + /// + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) + { + var node = base.CreateRootNode(queryStrings); + + if (IsDialog(queryStrings) && UserStartNodes.Contains(Constants.System.Root) == false) + { + node.AdditionalData["noAccess"] = true; + } + + return node; + } protected abstract TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings); + + /// + /// Returns a for the and + /// attaches some meta data to the node if the user doesn't have start node access to it when in dialog mode + /// + /// + /// + /// + /// + internal TreeNode GetSingleTreeNodeWithAccessCheck(IUmbracoEntity e, string parentId, FormDataCollection queryStrings) + { + bool hasPathAccess; + var entityIsAncestorOfStartNodes = Security.CurrentUser.IsInBranchOfStartNode(e, Services.EntityService, RecycleBinId, out hasPathAccess); + if (entityIsAncestorOfStartNodes == false) + return null; + + var treeNode = GetSingleTreeNode(e, parentId, queryStrings); + if (hasPathAccess == false) + { + treeNode.AdditionalData["noAccess"] = true; + } + return treeNode; + } /// /// Returns the @@ -69,13 +111,7 @@ namespace Umbraco.Web.Trees /// Returns the user's start node for this tree /// protected abstract int[] UserStartNodes { get; } - - /// - /// Gets the tree nodes for the given id - /// - /// - /// - /// + protected virtual TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings) { var nodes = new TreeNodeCollection(); @@ -83,9 +119,10 @@ namespace Umbraco.Web.Trees var altStartId = string.Empty; if (queryStrings.HasKey(TreeQueryStringParameters.StartNodeId)) altStartId = queryStrings.GetValue(TreeQueryStringParameters.StartNodeId); + var rootIdString = Constants.System.Root.ToString(CultureInfo.InvariantCulture); //check if a request has been made to render from a specific start node - if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != Constants.System.Root.ToString(CultureInfo.InvariantCulture)) + if (string.IsNullOrEmpty(altStartId) == false && altStartId != "undefined" && altStartId != rootIdString) { id = altStartId; @@ -111,10 +148,42 @@ namespace Umbraco.Web.Trees } } - var entities = GetChildEntities(id); - nodes.AddRange(entities.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null)); + var entities = GetChildEntities(id).ToList(); + + //If we are looking up the root and there is more than one node ... + //then we want to lookup those nodes' 'site' nodes and render those so that the + //user has some context of where they are in the tree, this is generally for pickers in a dialog. + //for any node they don't have access too, we need to add some metadata + if (id == rootIdString && entities.Count > 1) + { + var siteNodeIds = new List(); + //put into array since we might modify the list + foreach (var e in entities.ToArray()) + { + var pathParts = e.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); + if (pathParts.Length < 2) + continue; // this should never happen but better to check + + int siteNodeId; + if (int.TryParse(pathParts[1], out siteNodeId) == false) + continue; + + //we'll look up this + siteNodeIds.Add(siteNodeId); + } + var siteNodes = Services.EntityService.GetAll(UmbracoObjectType, siteNodeIds.ToArray()) + .DistinctBy(e => e.Id) + .ToArray(); + + //add site nodes + nodes.AddRange(siteNodes.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null)); + + return nodes; + } + + nodes.AddRange(entities.Select(e => GetSingleTreeNodeWithAccessCheck(e, id, queryStrings)).Where(node => node != null)); return nodes; - } + } protected abstract MenuItemCollection PerformGetMenuForNode(string id, FormDataCollection queryStrings); @@ -154,8 +223,21 @@ namespace Umbraco.Web.Trees /// /// /// + //we should remove this in v8, it's now here for backwards compat only protected abstract bool HasPathAccess(string id, FormDataCollection queryStrings); + /// + /// Returns true or false if the current user has access to the node based on the user's allowed start node (path) access + /// + /// + /// + /// + protected bool HasPathAccess(IUmbracoEntity entity, FormDataCollection queryStrings) + { + if (entity == null) return false; + return Security.CurrentUser.HasPathAccess(entity, Services.EntityService, RecycleBinId); + } + /// /// Ensures the recycle bin is appended when required (i.e. user has access to the root and it's not in dialog mode) /// @@ -214,7 +296,7 @@ namespace Umbraco.Web.Trees /// private TreeNodeCollection GetTreeNodesInternal(string id, FormDataCollection queryStrings) { - IUmbracoEntity current = GetEntityFromId(id); + var current = GetEntityFromId(id); //before we get the children we need to see if this is a container node @@ -242,7 +324,8 @@ namespace Umbraco.Web.Trees menu.Items.Add(ui.Text("actions", "emptyTrashcan")); menu.Items.Add(ui.Text("actions", ActionRefresh.Instance.Alias), true); return menu; - } + } + return PerformGetMenuForNode(id, queryStrings); } @@ -271,10 +354,11 @@ namespace Umbraco.Web.Trees internal IEnumerable GetAllowedUserMenuItemsForNode(IUmbracoEntity dd) { - var actions = global::umbraco.BusinessLogic.Actions.Action.FromString(UmbracoUser.GetPermissions(dd.Path)); - + var actions = ActionsResolver.Current.FromActionSymbols(Security.CurrentUser.GetPermissions(dd.Path, Services.UserService)) + .ToList(); + // A user is allowed to delete their own stuff - if (dd.CreatorId == UmbracoUser.Id && actions.Contains(ActionDelete.Instance) == false) + if (dd.CreatorId == Security.GetUserId() && actions.Contains(ActionDelete.Instance) == false) actions.Add(ActionDelete.Instance); return actions.Select(x => new MenuItem(x)); @@ -291,39 +375,78 @@ namespace Umbraco.Web.Trees { return allowedUserOptions.Select(x => x.Action).OfType().Any(); } - + /// - /// Get an entity via an id that can be either an integer, Guid or UDI + /// this will parse the string into either a GUID or INT /// /// /// - internal IUmbracoEntity GetEntityFromId(string id) + internal Tuple GetIdentifierFromString(string id) { - IUmbracoEntity entity; - Guid idGuid; int idInt; Udi idUdi; if (Guid.TryParse(id, out idGuid)) { - entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); + return new Tuple(idGuid, null); } - else if (int.TryParse(id, out idInt)) + if (int.TryParse(id, out idInt)) { - entity = Services.EntityService.Get(idInt, UmbracoObjectType); + return new Tuple(null, idInt); } - else if (Udi.TryParse(id, out idUdi)) + if (Udi.TryParse(id, out idUdi)) { var guidUdi = idUdi as GuidUdi; - entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null; - } - else - { - return null; - } + if (guidUdi != null) + return new Tuple(guidUdi.Guid, null); + } - return entity; - } + return null; + } + + /// + /// Get an entity via an id that can be either an integer, Guid or UDI + /// + /// + /// + /// + /// This object has it's own contextual cache for these lookups + /// + internal IUmbracoEntity GetEntityFromId(string id) + { + return _entityCache.GetOrAdd(id, s => + { + IUmbracoEntity entity; + + Guid idGuid; + int idInt; + Udi idUdi; + + if (Guid.TryParse(s, out idGuid)) + { + entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); + } + else if (int.TryParse(s, out idInt)) + { + entity = Services.EntityService.Get(idInt, UmbracoObjectType); + } + else if (Udi.TryParse(s, out idUdi)) + { + var guidUdi = idUdi as GuidUdi; + entity = guidUdi != null ? Services.EntityService.GetByKey(guidUdi.Guid, UmbracoObjectType) : null; + } + else + { + return null; + } + + return entity; + }); + + + } + + private readonly ConcurrentDictionary _entityCache = new ConcurrentDictionary(); } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 679d4a2d6a..99117b3b04 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -36,20 +36,7 @@ namespace Umbraco.Web.Trees public class MediaTreeController : ContentTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher(); - - protected override TreeNode CreateRootNode(FormDataCollection queryStrings) - { - var node = base.CreateRootNode(queryStrings); - - // if the user's start node is not default, then ensure the root doesn't have a menu - if (UserStartNodes.Contains(Constants.System.Root) == false) - { - node.MenuUrl = ""; - } - node.Name = ui.Text("sections", Constants.Trees.Media); - return node; - } - + protected override int RecycleBinId { get { return Constants.System.RecycleBinMedia; } @@ -107,9 +94,12 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { - //if the user's start node is not the root then ensure the root menu is empty/doesn't exist + // if the user's start node is not the root then the only menu item to display is refresh if (UserStartNodes.Contains(Constants.System.Root) == false) { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); return menu; } @@ -130,6 +120,16 @@ namespace Umbraco.Web.Trees { throw new HttpResponseException(HttpStatusCode.NotFound); } + + //if the user has no path access for this node, all they can do is refresh + if (Security.CurrentUser.HasPathAccess(item, Services.EntityService, RecycleBinId) == false) + { + menu.Items.Add( + Services.TextService.Localize(string.Concat("actions/", ActionRefresh.Instance.Alias)), + true); + return menu; + } + //return a normal node menu: menu.Items.Add(ui.Text("actions", ActionNew.Instance.Alias)); menu.Items.Add(ui.Text("actions", ActionMove.Instance.Alias)); @@ -160,17 +160,7 @@ namespace Umbraco.Web.Trees protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { var entity = GetEntityFromId(id); - if (entity == null) - { - return false; - } - - var media = Services.MediaService.GetById(entity.Id); - if (media == null) - { - return false; - } - return Security.CurrentUser.HasPathAccess(media, Services.EntityService); + return HasPathAccess(entity, queryStrings); } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) diff --git a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs index c09fd0fa0c..c78f66ab97 100644 --- a/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs +++ b/src/Umbraco.Web/UI/Pages/UmbracoEnsuredPage.cs @@ -7,7 +7,9 @@ using Umbraco.Web.Security; using umbraco; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; +using umbraco.interfaces; using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Security; namespace Umbraco.Web.UI.Pages @@ -32,6 +34,40 @@ namespace Umbraco.Web.UI.Pages } } + /// + /// Performs an authorization check for the user against the requested entity/path and permission set, this is only relevant to content and media + /// + /// + /// + /// + protected void CheckPathAndPermissions(int entityId, UmbracoObjectTypes objectType, IAction actionToCheck) + { + if (objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) + { + //check path access + + var entity = entityId == Constants.System.Root + ? UmbracoEntity.Root + : Services.EntityService.Get( + entityId, + objectType); + var hasAccess = Security.CurrentUser.HasPathAccess( + entity, + Services.EntityService, + objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); + if (hasAccess == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have access to the path '{0}'", entity.Path)); + + //only documents have action permissions + if (objectType == UmbracoObjectTypes.Document) + { + var allowedActions = ActionsResolver.Current.FromActionSymbols(Security.CurrentUser.GetPermissions(entity.Path, Services.UserService)).ToArray(); + if (allowedActions.Contains(actionToCheck) == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have permission to {0} on the path '{1}'", actionToCheck.Alias, entity.Path)); + } + } + } + private bool _hasValidated = false; /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c9cb749d28..e5b2568430 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1138,9 +1138,6 @@ - - ASPXCodeBehind - ASPXCodeBehind diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs index 4f71e20d08..de1e563776 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -15,6 +15,14 @@ namespace umbraco.dialogs { public partial class AssignDomain2 : UmbracoEnsuredPage { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + var nodeId = GetNodeId(); + CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); + } + protected override void OnLoad(EventArgs e) { base.OnLoad(e); @@ -29,16 +37,7 @@ namespace umbraco.dialogs pane_domains.Visible = false; p_buttons.Visible = false; return; - } - - if (UmbracoUser.GetPermissions(node.Path).Contains(ActionAssignDomain.Instance.Letter) == false) - { - feedback.Text = ui.Text("assignDomain", "permissionDenied"); - pane_language.Visible = false; - pane_domains.Visible = false; - p_buttons.Visible = false; - return; - } + } pane_language.Title = ui.Text("assignDomain", "setLanguage"); pane_domains.Title = ui.Text("assignDomain", "setDomains"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs deleted file mode 100644 index f8fc1d3ea3..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/moveOrCopy.aspx.cs +++ /dev/null @@ -1,442 +0,0 @@ -using System; -using System.Collections; -using System.Globalization; -using System.Web.UI; -using System.Web.UI.WebControls; -using System.Xml; -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Models; -using umbraco.BasePages; -using System.Linq; -using umbraco.interfaces; -using Umbraco.Web; -using Umbraco.Core; - -namespace umbraco.dialogs -{ - /// - /// Summary description for moveOrCopy. - /// - public partial class moveOrCopy : UmbracoEnsuredPage - { - - protected override void OnInit(EventArgs e) - { - CurrentApp = Request["app"]; - - base.OnInit(e); - } - - protected void Page_Load(object sender, EventArgs e) - { - JTree.DataBind(); - - // Put user code to initialize the page here - if (IsPostBack == false) - { - pp_relate.Text = ui.Text("moveOrCopy", "relateToOriginal"); - - //Document Type copy Hack... - - if (CurrentApp == Constants.Applications.Settings) - { - pane_form.Visible = false; - pane_form_notice.Visible = false; - pane_settings.Visible = true; - - ok.Text = ui.Text("general", "ok", UmbracoUser); - ok.Attributes.Add("style", "width: 60px"); - - var documentType = Services.ContentTypeService.GetContentType(int.Parse(Request.GetItemAsString("id"))); - - //Load master types... - masterType.Attributes.Add("style", "width: 350px;"); - masterType.Items.Add(new ListItem(ui.Text("none") + "...", "0")); - - foreach (var docT in Services.ContentTypeService.GetAllContentTypes().OrderBy(x => x.Name)) - { - masterType.Items.Add(new ListItem(docT.Name, docT.Id.ToString(CultureInfo.InvariantCulture))); - } - - masterType.SelectedValue = (documentType.ParentId > 0 ? documentType.ParentId : 0).ToString(CultureInfo.InvariantCulture); - - rename.Text = documentType.Name + " (copy)"; - pane_settings.Text = "Make a copy of the document type '" + documentType.Name + "' and save it under a new name"; - - } - else - { - pane_form.Visible = true; - pane_form_notice.Visible = true; - - pane_settings.Visible = false; - - // Caption and properies on BUTTON - ok.Text = ui.Text("general", "ok", UmbracoUser); - ok.Attributes.Add("style", "width: 60px"); - ok.Attributes.Add("disabled", "true"); - - IContentBase currContent; - if (CurrentApp == "content") - { - currContent = Services.ContentService.GetById(Request.GetItemAs("id")); - } - else - { - currContent = Services.MediaService.GetById(Request.GetItemAs("id")); - } - - // Preselect the parent of the seslected item. - if (currContent.ParentId > 0) - JTree.SelectedNodePath = currContent.Path.Substring(0, currContent.Path.LastIndexOf(',')); - - var validAction = true; - if (CurrentApp == Constants.Applications.Content && Umbraco.Core.Models.ContentExtensions.HasChildren(currContent, Services)) - { - validAction = ValidAction(currContent, Request.GetItemAsString("mode") == "cut" ? 'M' : 'O'); - } - - if (Request.GetItemAsString("mode") == "cut") - { - pane_form.Text = ui.Text("moveOrCopy", "moveTo", currContent.Name, UmbracoUser); - pp_relate.Visible = false; - } - else - { - pane_form.Text = ui.Text("moveOrCopy", "copyTo", currContent.Name, UmbracoUser); - pp_relate.Visible = true; - } - - if (validAction == false) - { - panel_buttons.Visible = false; - ScriptManager.RegisterStartupScript(this, GetType(), "notvalid", "notValid();", true); - } - } - } - - } - - private bool ValidAction(IContentBase cmsNode, char actionLetter) - { - var currentAction = BusinessLogic.Actions.Action.GetPermissionAssignable().First(a => a.Letter == actionLetter); - return CheckPermissions(cmsNode, currentAction); - } - - /// - /// Checks if the current user has permissions to execute this action against this node - /// - /// - /// - /// - /// - /// This used to do a recursive check for all descendent nodes but this is not required and is a massive CPU hog. - /// See: http://issues.umbraco.org/issue/U4-2632, https://groups.google.com/forum/?fromgroups=#!topic/umbraco-dev/L1D4LwVSP2Y - /// - private bool CheckPermissions(IContentBase node, IAction currentAction) - { - var userService = ApplicationContext.Current.Services.UserService; - var currUserPermissions = userService.GetPermissions(UmbracoContext.Current.Security.CurrentUser, node.Id).GetAllPermissions(); - return currUserPermissions != null && currUserPermissions.Contains(currentAction.Letter.ToString(CultureInfo.InvariantCulture)); - } - - private void HandleDocumentTypeCopy() - { - var contentTypeService = ApplicationContext.Current.Services.ContentTypeService; - var contentType = contentTypeService.GetContentType( - int.Parse(Request.GetItemAsString("id"))); - - //set the master - //http://issues.umbraco.org/issue/U4-2843 - //http://issues.umbraco.org/issue/U4-3552 - var parentId = int.Parse(masterType.SelectedValue); - - var alias = rename.Text.Trim().Replace("'", "''"); - var clone = contentTypeService.Copy(contentType, alias, rename.Text.Trim(), parentId); - - var returnUrl = string.Format("{0}/settings/editNodeTypeNew.aspx?id={1}", SystemDirectories.Umbraco, clone.Id); - - pane_settings.Visible = false; - panel_buttons.Visible = false; - - feedback.Text = "Document type copied"; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - ClientTools.ChangeContentFrameUrl(returnUrl); - } - - public void HandleMoveOrCopy(object sender, EventArgs e) - { - if (CurrentApp == Constants.Applications.Settings) - HandleDocumentTypeCopy(); - else - HandleDocumentMoveOrCopy(); - } - - protected override void OnPreRender(EventArgs e) - { - base.OnPreRender(e); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/cmsnode.asmx")); - ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference("../webservices/legacyAjaxCalls.asmx")); - } - - private void HandleDocumentMoveOrCopy() - { - if (Request.GetItemAsString("copyTo") != "" && Request.GetItemAsString("id") != "") - { - // Check if the current node is allowed at new position - var nodeAllowed = false; - - IContentBase currContent; - IContentBase parentContent = null; - IContentTypeBase parentContentType = null; - if (CurrentApp == "content") - { - currContent = Services.ContentService.GetById(Request.GetItemAs("id")); - if (Request.GetItemAs("copyTo") != -1) - { - parentContent = Services.ContentService.GetById(Request.GetItemAs("copyTo")); - if (parentContent != null) - { - parentContentType = Services.ContentTypeService.GetContentType(parentContent.ContentTypeId); - } - } - } - else - { - currContent = Services.MediaService.GetById(Request.GetItemAs("id")); - if (Request.GetItemAs("copyTo") != -1) - { - parentContent = Services.MediaService.GetById(Request.GetItemAs("copyTo")); - if (parentContent != null) - { - parentContentType = Services.ContentTypeService.GetMediaType(parentContent.ContentTypeId); - } - } - } - - // Check on contenttypes - if (parentContentType == null) - { - //check if this is allowed at root - IContentTypeBase currContentType; - if (CurrentApp == "content") - { - currContentType = Services.ContentTypeService.GetContentType(currContent.ContentTypeId); - } - else - { - currContentType = Services.ContentTypeService.GetMediaType(currContent.ContentTypeId); - } - nodeAllowed = currContentType.AllowedAsRoot; - if (!nodeAllowed) - { - feedback.Text = ui.Text("moveOrCopy", "notAllowedAtRoot", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - } - else - { - var allowedChildContentTypeIds = parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray(); - if (allowedChildContentTypeIds.Any(x => x.Value == currContent.ContentTypeId)) - { - nodeAllowed = true; - } - - if (nodeAllowed == false) - { - feedback.Text = ui.Text("moveOrCopy", "notAllowedByContentType", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - else - { - // Check on paths - if ((string.Format(",{0},", parentContent.Path)).IndexOf(string.Format(",{0},", currContent.Id)) > -1) - { - nodeAllowed = false; - feedback.Text = ui.Text("moveOrCopy", "notAllowedByPath", UmbracoUser); - feedback.type = uicontrols.Feedback.feedbacktype.error; - } - } - } - - if (nodeAllowed) - { - pane_form.Visible = false; - pane_form_notice.Visible = false; - panel_buttons.Visible = false; - - var newNodeCaption = parentContent == null - ? ui.Text(CurrentApp) - : parentContent.Name; - - string[] nodes = { currContent.Name, newNodeCaption }; - - if (Request["mode"] == "cut") - { - if (CurrentApp == Constants.Applications.Content) - { - var doc = (IContent)currContent; - var copyToId = Request.GetItemAs("copyTo"); - Services.ContentService.Move(doc, copyToId, UmbracoUser.Id); - - } - else - { - var media = (IMedia)currContent; - var copyToId = Request.GetItemAs("copyTo"); - Services.MediaService.Move(media, copyToId, UmbracoUser.Id); - } - - feedback.Text = ui.Text("moveOrCopy", "moveDone", nodes, UmbracoUser) + "

    " + ui.Text("closeThisWindow") + ""; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - // refresh tree - ClientTools.MoveNode(currContent.Id.ToString(), currContent.Path); - } - else - { - //NOTE: We ONLY support Copy on content not media for some reason. - - var newContent = (IContent)currContent; - Services.ContentService.Copy(newContent, Request.GetItemAs("copyTo"), RelateDocuments.Checked, UmbracoUser.Id); - - feedback.Text = ui.Text("moveOrCopy", "copyDone", nodes, UmbracoUser) + "

    " + ui.Text("closeThisWindow") + ""; - feedback.type = uicontrols.Feedback.feedbacktype.success; - - // refresh tree - ClientTools.CopyNode(currContent.Id.ToString(), newContent.Path); - } - } - } - } - - ///

    - /// JsInclude1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::ClientDependency.Core.Controls.JsInclude JsInclude1; - - /// - /// feedback control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Feedback feedback; - - /// - /// pane_form control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_form; - - /// - /// JTree control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.controls.Tree.TreeControl JTree; - - /// - /// pp_relate control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel pp_relate; - - /// - /// RelateDocuments control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CheckBox RelateDocuments; - - /// - /// pane_form_notice control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.PlaceHolder pane_form_notice; - - /// - /// pane_settings control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.Pane pane_settings; - - /// - /// PropertyPanel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::umbraco.uicontrols.PropertyPanel PropertyPanel1; - - /// - /// masterType control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.ListBox masterType; - - /// - /// rename control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox rename; - - /// - /// RequiredFieldValidator1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator1; - - /// - /// panel_buttons control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Panel panel_buttons; - - /// - /// ok control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button ok; - - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs index f3dbb43519..f3bb4c8fdc 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/sort.aspx.cs @@ -13,6 +13,8 @@ using umbraco.cms.businesslogic.media; using umbraco.cms.businesslogic.web; using System.Web.UI; using System.Collections.Generic; +using umbraco.businesslogic.Exceptions; +using Umbraco.Core.Models; namespace umbraco.cms.presentation { @@ -20,7 +22,13 @@ namespace umbraco.cms.presentation /// Summary description for sort. ///
    public partial class sort : UmbracoEnsuredPage - { + { + /// + /// The Parent Id being sorted + /// + protected int? ParentIdAsInt { get; private set; } + protected string ParentIdAsString { get; private set; } + private readonly List _nodes = new List(); protected bool HideDateColumn @@ -33,6 +41,21 @@ namespace umbraco.cms.presentation { CurrentApp = helper.Request("app"); + ParentIdAsString = Request.GetItemAsString("ID"); + int parentId; + if (int.TryParse(ParentIdAsString, out parentId)) + { + ParentIdAsInt = parentId; + + if (CurrentApp == Constants.Applications.Content || CurrentApp == Constants.Applications.Media) + { + CheckPathAndPermissions( + ParentIdAsInt.Value, + CurrentApp == Constants.Applications.Content ? UmbracoObjectTypes.Document : UmbracoObjectTypes.Media, + ActionSort.Instance); + } + } + base.OnInit(e); } @@ -50,23 +73,22 @@ namespace umbraco.cms.presentation var app = Request.GetItemAsString("app"); var icon = "../images/umbraco/doc.gif"; - - int parentId; - if (int.TryParse(Request.GetItemAsString("ID"), out parentId)) + + if (ParentIdAsInt.HasValue) { if (app == Constants.Applications.Media) { icon = "../images/umbraco/mediaPhoto.gif"; var mediaService = ApplicationContext.Current.Services.MediaService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in mediaService.GetRootMedia().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = mediaService.GetChildren(parentId); + var children = mediaService.GetChildren(ParentIdAsInt.Value); foreach (var child in children.OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -76,14 +98,14 @@ namespace umbraco.cms.presentation { var contentService = ApplicationContext.Current.Services.ContentService; - if (parentId == -1) + if (ParentIdAsInt.Value == -1) { foreach (var child in contentService.GetRootContent().ToList().OrderBy(x => x.SortOrder)) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } else { - var children = contentService.GetChildren(parentId); + var children = contentService.GetChildren(ParentIdAsInt.Value); foreach (var child in children) _nodes.Add(CreateNode(child.Id.ToInvariantString(), child.SortOrder, child.Name, child.CreateDate, icon)); } @@ -100,7 +122,7 @@ namespace umbraco.cms.presentation HideDateColumn = true; - var stylesheetName = Request.GetItemAsString("ID"); + var stylesheetName = ParentIdAsString; if (stylesheetName.IsNullOrWhiteSpace())throw new NullReferenceException("No Id passed in to editor"); var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); diff --git a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs index 58362d50f2..36a0f63f36 100644 --- a/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs +++ b/src/umbraco.businesslogic/BasePages/UmbracoEnsuredPage.cs @@ -7,6 +7,8 @@ using Umbraco.Core.IO; using Umbraco.Core.Logging; using umbraco.BusinessLogic; using umbraco.businesslogic.Exceptions; +using umbraco.interfaces; +using Umbraco.Core.Models; using Umbraco.Core.Security; namespace umbraco.BasePages @@ -16,7 +18,41 @@ namespace umbraco.BasePages ///
    [Obsolete("This class has been superceded by Umbraco.Web.UI.Pages.UmbracoEnsuredPage")] public class UmbracoEnsuredPage : BasePage - { + { + /// + /// Performs an authorization check for the user against the requested entity/path and permission set, this is only relevant to content and media + /// + /// + /// + /// + protected void CheckPathAndPermissions(int entityId, UmbracoObjectTypes objectType, IAction actionToCheck) + { + if (objectType == UmbracoObjectTypes.Document || objectType == UmbracoObjectTypes.Media) + { + //check path access + + var entity = entityId == Constants.System.Root + ? UmbracoEntity.Root + : Services.EntityService.Get( + entityId, + objectType); + var hasAccess = CurrentUser.UserEntity.HasPathAccess( + entity, + Services.EntityService, + objectType == UmbracoObjectTypes.Document ? Constants.System.RecycleBinContent : Constants.System.RecycleBinMedia); + if (hasAccess == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have access to the path '{0}'", entity.Path)); + + //only documents have action permissions + if (objectType == UmbracoObjectTypes.Document) + { + var allowedActions = ActionsResolver.Current.FromActionSymbols(CurrentUser.UserEntity.GetPermissions(entity.Path, Services.UserService)).ToArray(); + if (allowedActions.Contains(actionToCheck) == false) + throw new UserAuthorizationException(string.Format("The current user doesn't have permission to {0} on the path '{1}'", actionToCheck.Alias, entity.Path)); + } + } + } + /// /// Checks if the page exists outside of the /umbraco route, in which case the request will not have been authenticated for the back office /// so we'll force authentication. diff --git a/src/umbraco.cms/Actions/Action.cs b/src/umbraco.cms/Actions/Action.cs index 81c78ade1b..c7a9a6242b 100644 --- a/src/umbraco.cms/Actions/Action.cs +++ b/src/umbraco.cms/Actions/Action.cs @@ -128,30 +128,20 @@ namespace umbraco.BusinessLogic.Actions /// /// /// returns a list of actions that have an associated letter found in the action string list + [Obsolete("Use ActionsResolver.Current.FromActionSymbols instead")] public static List FromString(string actions) { - List list = new List(); - foreach (char c in actions.ToCharArray()) - { - IAction action = ActionsResolver.Current.Actions.ToList().Find( - delegate(IAction a) - { - return a.Letter == c; - } - ); - if (action != null) - list.Add(action); - } - return list; + return ActionsResolver.Current.FromActionSymbols(actions.ToCharArray().Select(x => x.ToString())).ToList(); } /// /// Returns the string representation of the actions that make up the actions collection /// /// + [Obsolete("Use ActionsResolver.Current.ToActionSymbols instead")] public static string ToString(List actions) { - string[] strMenu = Array.ConvertAll(actions.ToArray(), delegate(IAction a) { return (a.Letter.ToString(CultureInfo.InvariantCulture)); }); + string[] strMenu = Array.ConvertAll(actions.ToArray(), a => (a.Letter.ToString(CultureInfo.InvariantCulture))); return string.Join("", strMenu); } @@ -161,12 +151,7 @@ namespace umbraco.BusinessLogic.Actions /// public static List GetPermissionAssignable() { - return ActionsResolver.Current.Actions.ToList().FindAll( - delegate(IAction a) - { - return (a.CanBePermissionAssigned); - } - ); + return ActionsResolver.Current.Actions.ToList().FindAll(a => (a.CanBePermissionAssigned)); } ///