Merge pull request #1149 from umbraco/temp-U4-4700

U4-4700 List View Doesn't Respect User Permissions
This commit is contained in:
Warren Buckley
2016-03-10 08:52:38 +00:00
11 changed files with 408 additions and 276 deletions

View File

@@ -509,6 +509,16 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
'Failed to check permission for item ' + id);
},
getPermissions: function (nodeIds) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetPermissions"),
nodeIds),
'Failed to get permissions');
},
/**
* @ngdoc method
* @name umbraco.resources.contentResource#save

View File

@@ -1,288 +1,319 @@
(function() {
'use strict';
(function () {
'use strict';
function listViewHelper(localStorageService) {
function listViewHelper(localStorageService) {
var firstSelectedIndex = 0;
var localStorageKey = "umblistViewLayout";
var firstSelectedIndex = 0;
var localStorageKey = "umblistViewLayout";
function getLayout(nodeId, availableLayouts) {
function getLayout(nodeId, availableLayouts) {
var storedLayouts = [];
var storedLayouts = [];
if(localStorageService.get(localStorageKey)) {
storedLayouts = localStorageService.get(localStorageKey);
}
if (storedLayouts && storedLayouts.length > 0) {
for (var i = 0; storedLayouts.length > i; i++) {
var layout = storedLayouts[i];
if (layout.nodeId === nodeId) {
return setLayout(nodeId, layout, availableLayouts);
}
}
}
return getFirstAllowedLayout(availableLayouts);
}
function setLayout(nodeId, selectedLayout, availableLayouts) {
var activeLayout = {};
var layoutFound = false;
for (var i = 0; availableLayouts.length > i; i++) {
var layout = availableLayouts[i];
if (layout.path === selectedLayout.path) {
activeLayout = layout;
layout.active = true;
layoutFound = true;
} else {
layout.active = false;
}
}
if(!layoutFound) {
activeLayout = getFirstAllowedLayout(availableLayouts);
}
saveLayoutInLocalStorage(nodeId, activeLayout);
return activeLayout;
}
function saveLayoutInLocalStorage(nodeId, selectedLayout) {
var layoutFound = false;
var storedLayouts = [];
if(localStorageService.get(localStorageKey)) {
storedLayouts = localStorageService.get(localStorageKey);
}
if(storedLayouts.length > 0) {
for(var i = 0; storedLayouts.length > i; i++) {
var layout = storedLayouts[i];
if(layout.nodeId === nodeId) {
layout.path = selectedLayout.path;
layoutFound = true;
}
}
}
if(!layoutFound) {
var storageObject = {
"nodeId": nodeId,
"path": selectedLayout.path
};
storedLayouts.push(storageObject);
}
localStorageService.set(localStorageKey, storedLayouts);
}
function getFirstAllowedLayout(layouts) {
var firstAllowedLayout = {};
for (var i = 0; layouts.length > i; i++) {
var layout = layouts[i];
if (layout.selected === true) {
firstAllowedLayout = layout;
break;
if (localStorageService.get(localStorageKey)) {
storedLayouts = localStorageService.get(localStorageKey);
}
}
return firstAllowedLayout;
}
if (storedLayouts && storedLayouts.length > 0) {
for (var i = 0; storedLayouts.length > i; i++) {
var layout = storedLayouts[i];
if (layout.nodeId === nodeId) {
return setLayout(nodeId, layout, availableLayouts);
}
}
function selectHandler(selectedItem, selectedIndex, items, selection, $event) {
}
var start = 0;
var end = 0;
var item = null;
return getFirstAllowedLayout(availableLayouts);
if ($event.shiftKey === true) {
}
if(selectedIndex > firstSelectedIndex) {
function setLayout(nodeId, selectedLayout, availableLayouts) {
start = firstSelectedIndex;
end = selectedIndex;
var activeLayout = {};
var layoutFound = false;
for (; end >= start; start++) {
item = items[start];
selectItem(item, selection);
}
for (var i = 0; availableLayouts.length > i; i++) {
var layout = availableLayouts[i];
if (layout.path === selectedLayout.path) {
activeLayout = layout;
layout.active = true;
layoutFound = true;
} else {
layout.active = false;
}
}
if (!layoutFound) {
activeLayout = getFirstAllowedLayout(availableLayouts);
}
saveLayoutInLocalStorage(nodeId, activeLayout);
return activeLayout;
}
function saveLayoutInLocalStorage(nodeId, selectedLayout) {
var layoutFound = false;
var storedLayouts = [];
if (localStorageService.get(localStorageKey)) {
storedLayouts = localStorageService.get(localStorageKey);
}
if (storedLayouts.length > 0) {
for (var i = 0; storedLayouts.length > i; i++) {
var layout = storedLayouts[i];
if (layout.nodeId === nodeId) {
layout.path = selectedLayout.path;
layoutFound = true;
}
}
}
if (!layoutFound) {
var storageObject = {
"nodeId": nodeId,
"path": selectedLayout.path
};
storedLayouts.push(storageObject);
}
localStorageService.set(localStorageKey, storedLayouts);
}
function getFirstAllowedLayout(layouts) {
var firstAllowedLayout = {};
for (var i = 0; layouts.length > i; i++) {
var layout = layouts[i];
if (layout.selected === true) {
firstAllowedLayout = layout;
break;
}
}
return firstAllowedLayout;
}
function selectHandler(selectedItem, selectedIndex, items, selection, $event) {
var start = 0;
var end = 0;
var item = null;
if ($event.shiftKey === true) {
if (selectedIndex > firstSelectedIndex) {
start = firstSelectedIndex;
end = selectedIndex;
for (; end >= start; start++) {
item = items[start];
selectItem(item, selection);
}
} else {
start = firstSelectedIndex;
end = selectedIndex;
for (; end <= start; start--) {
item = items[start];
selectItem(item, selection);
}
}
} else {
start = firstSelectedIndex;
end = selectedIndex;
if (selectedItem.selected) {
deselectItem(selectedItem, selection);
} else {
selectItem(selectedItem, selection);
}
for (; end <= start; start--) {
item = items[start];
selectItem(item, selection);
}
firstSelectedIndex = selectedIndex;
}
} else {
}
if(selectedItem.selected) {
deselectItem(selectedItem, selection);
} else {
selectItem(selectedItem, selection);
function selectItem(item, selection) {
var isSelected = false;
for (var i = 0; selection.length > i; i++) {
var selectedItem = selection[i];
if (item.id === selectedItem.id) {
isSelected = true;
}
}
if (!isSelected) {
selection.push({ id: item.id });
item.selected = true;
}
}
function deselectItem(item, selection) {
for (var i = 0; selection.length > i; i++) {
var selectedItem = selection[i];
if (item.id === selectedItem.id) {
selection.splice(i, 1);
item.selected = false;
}
}
}
function clearSelection(items, folders, selection) {
var i = 0;
selection.length = 0;
if (angular.isArray(items)) {
for (i = 0; items.length > i; i++) {
var item = items[i];
item.selected = false;
}
}
firstSelectedIndex = selectedIndex;
}
}
function selectItem(item, selection) {
var isSelected = false;
for (var i = 0; selection.length > i; i++) {
var selectedItem = selection[i];
if (item.id === selectedItem.id) {
isSelected = true;
if (angular.isArray(items)) {
for (i = 0; folders.length > i; i++) {
var folder = folders[i];
folder.selected = false;
}
}
}
if(!isSelected) {
selection.push({id: item.id});
item.selected = true;
}
}
}
function deselectItem(item, selection) {
for (var i = 0; selection.length > i; i++) {
var selectedItem = selection[i];
if (item.id === selectedItem.id) {
selection.splice(i, 1);
item.selected = false;
function selectAllItems(items, selection, $event) {
var checkbox = $event.target;
var clearSelection = false;
if (!angular.isArray(items)) {
return;
}
}
}
function clearSelection(items, folders, selection) {
selection.length = 0;
var i = 0;
for (var i = 0; i < items.length; i++) {
selection.length = 0;
var item = items[i];
if (checkbox.checked) {
selection.push({ id: item.id });
} else {
clearSelection = true;
}
item.selected = checkbox.checked;
if(angular.isArray(items)) {
for(i = 0; items.length > i; i++) {
var item = items[i];
item.selected = false;
}
}
if(angular.isArray(items)) {
for(i = 0; folders.length > i; i++) {
var folder = folders[i];
folder.selected = false;
if (clearSelection) {
selection.length = 0;
}
}
}
function selectAllItems(items, selection, $event) {
}
var checkbox = $event.target;
var clearSelection = false;
function isSelectedAll(items, selection) {
if (!angular.isArray(items)) {
return;
}
var numberOfSelectedItem = 0;
selection.length = 0;
for (var itemIndex = 0; items.length > itemIndex; itemIndex++) {
var item = items[itemIndex];
for (var i = 0; i < items.length; i++) {
for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) {
var selectedItem = selection[selectedIndex];
var item = items[i];
if (item.id === selectedItem.id) {
numberOfSelectedItem++;
}
}
if (checkbox.checked) {
selection.push({id: item.id});
} else {
clearSelection = true;
}
}
item.selected = checkbox.checked;
if (numberOfSelectedItem === items.length) {
return true;
}
}
}
if (clearSelection) {
selection.length = 0;
}
function setSortingDirection(col, direction, options) {
return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction;
}
}
function setSorting(field, allow, options) {
if (allow) {
options.orderBy = field;
function isSelectedAll(items, selection) {
if (options.orderDirection === "desc") {
options.orderDirection = "asc";
} else {
options.orderDirection = "desc";
}
}
}
//This takes in a dictionary of Ids with Permissions and determines
// the intersect of all permissions to return an object representing the
// listview button permissions
function getButtonPermissions(unmergedPermissions, currentIdsWithPermissions) {
if (currentIdsWithPermissions == null) {
currentIdsWithPermissions = {};
}
var numberOfSelectedItem = 0;
//merge the newly retrieved permissions to the main dictionary
_.each(unmergedPermissions, function (value, key, list) {
currentIdsWithPermissions[key] = value;
});
for(var itemIndex = 0; items.length > itemIndex; itemIndex++) {
var item = items[itemIndex];
//get the intersect permissions
var arr = [];
_.each(currentIdsWithPermissions, function (value, key, list) {
arr.push(value);
});
for(var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) {
var selectedItem = selection[selectedIndex];
//we need to use 'apply' to call intersection with an array of arrays,
//see: http://stackoverflow.com/a/16229480/694494
var intersectPermissions = _.intersection.apply(_, arr);
if(item.id === selectedItem.id) {
numberOfSelectedItem++;
}
}
return {
canCopy: _.contains(intersectPermissions, 'O'), //Magic Char = O
canCreate: _.contains(intersectPermissions, 'C'), //Magic Char = C
canDelete: _.contains(intersectPermissions, 'D'), //Magic Char = D
canMove: _.contains(intersectPermissions, 'M'), //Magic Char = M
canPublish: _.contains(intersectPermissions, 'U'), //Magic Char = U
canUnpublish: _.contains(intersectPermissions, 'U'), //Magic Char = Z (however UI says it can't be set, so if we can publish 'U' we can unpublish)
};
}
}
var service = {
getLayout: getLayout,
getFirstAllowedLayout: getFirstAllowedLayout,
setLayout: setLayout,
saveLayoutInLocalStorage: saveLayoutInLocalStorage,
selectHandler: selectHandler,
selectItem: selectItem,
deselectItem: deselectItem,
clearSelection: clearSelection,
selectAllItems: selectAllItems,
isSelectedAll: isSelectedAll,
setSortingDirection: setSortingDirection,
setSorting: setSorting,
getButtonPermissions: getButtonPermissions
};
if(numberOfSelectedItem === items.length) {
return true;
}
return service;
}
}
function setSortingDirection(col, direction, options) {
return options.orderBy.toUpperCase() === col.toUpperCase() && options.orderDirection === direction;
}
function setSorting(field, allow, options) {
if (allow) {
options.orderBy = field;
if (options.orderDirection === "desc") {
options.orderDirection = "asc";
} else {
options.orderDirection = "desc";
}
}
}
var service = {
getLayout: getLayout,
getFirstAllowedLayout: getFirstAllowedLayout,
setLayout: setLayout,
saveLayoutInLocalStorage: saveLayoutInLocalStorage,
selectHandler: selectHandler,
selectItem: selectItem,
deselectItem: deselectItem,
clearSelection: clearSelection,
selectAllItems: selectAllItems,
isSelectedAll: isSelectedAll,
setSortingDirection: setSortingDirection,
setSorting: setSorting
};
return service;
}
angular.module('umbraco.services').factory('listViewHelper', listViewHelper);
angular.module('umbraco.services').factory('listViewHelper', listViewHelper);
})();

View File

@@ -21,6 +21,7 @@ function packageHelper(assetsService, treeService, eventsService, $templateCache
}
angular.module('umbraco.services').factory('packageHelper', packageHelper);
//TODO: I believe this is obsolete
function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) {
return {
/** sets the image's url, thumbnail and if its a folder */
@@ -319,7 +320,6 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me
}
};
}
angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper);
/**

View File

@@ -4,8 +4,8 @@ function booleanEditorController($scope, $rootScope, assetsService) {
$scope.renderModel = {
value: false
};
if($scope.model.config.default.toString() === "1" && $scope.model && !$scope.model.value) {
if ($scope.model.config.default && $scope.model.config.default.toString() === "1" && $scope.model && !$scope.model.value) {
$scope.renderModel.value = true;
}

View File

@@ -1,4 +1,4 @@
function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper) {
function listViewController($rootScope, $scope, $routeParams, $injector, $cookieStore, notificationsService, iconHelper, dialogService, editorState, localizationService, $location, appState, $timeout, $q, mediaResource, listViewHelper, userService) {
//this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content
// that isn't created yet, if we continue this will use the parent id in the route params which isn't what
@@ -58,6 +58,63 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie
totalPages: 0,
items: []
};
//when this is null, we don't check permissions
$scope.buttonPermissions = null;
//When we are dealing with 'content', we need to deal with permissions on child nodes.
// Currently there is no real good way to
if ($scope.entityType === "content") {
var idsWithPermissions = null;
$scope.buttonPermissions = {
canCopy: true,
canCreate: true,
canDelete: true,
canMove: true,
canPublish: true,
canUnpublish: true
};
$scope.$watch(function() {
return $scope.selection.length;
}, function(newVal, oldVal) {
if ((idsWithPermissions == null && newVal > 0) || (idsWithPermissions != null)) {
//get all of the selected ids
var ids = _.map($scope.selection, function(i) {
return i.id.toString();
});
//remove the dictionary items that don't have matching ids
var filtered = {};
_.each(idsWithPermissions, function (value, key, list) {
if (_.contains(ids, key)) {
filtered[key] = value;
}
});
idsWithPermissions = filtered;
//find all ids that we haven't looked up permissions for
var existingIds = _.keys(idsWithPermissions);
var missingLookup = _.map(_.difference(ids, existingIds), function (i) {
return Number(i);
});
if (missingLookup.length > 0) {
contentResource.getPermissions(missingLookup).then(function(p) {
$scope.buttonPermissions = listViewHelper.getButtonPermissions(p, idsWithPermissions);
});
}
else {
$scope.buttonPermissions = listViewHelper.getButtonPermissions({}, idsWithPermissions);
}
}
});
}
$scope.options = {
displayAtTabNumber: $scope.model.config.displayAtTabNumber ? $scope.model.config.displayAtTabNumber : 1,
@@ -166,9 +223,8 @@ function listViewController($rootScope, $scope, $routeParams, $injector, $cookie
getListResultsCallback(id, $scope.options).then(function(data) {
$scope.actionInProgress = false;
$scope.listViewResultSet = data;
//update all values for display
if ($scope.listViewResultSet.items) {
_.each($scope.listViewResultSet.items, function(e, index) {

View File

@@ -10,7 +10,7 @@
<umb-editor-sub-header-content-left>
<umb-editor-sub-header-section ng-if="listViewAllowedTypes && listViewAllowedTypes.length > 0 && !isAnythingSelected()">
<umb-editor-sub-header-section ng-if="(listViewAllowedTypes && listViewAllowedTypes.length > 0 && !isAnythingSelected()) && currentNodePermissions.canCreate">
<div class="btn-group">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
<localize key="actions_create">Create</localize>
@@ -82,7 +82,7 @@
<umb-editor-sub-header-section ng-if="isAnythingSelected()">
<umb-button
ng-if="options.allowBulkPublish"
ng-if="options.allowBulkPublish && (buttonPermissions == null || buttonPermissions.canPublish)"
type="button"
button-style="link"
label="Publish"
@@ -93,7 +93,7 @@
</umb-button>
<umb-button
ng-if="options.allowBulkUnpublish"
ng-if="options.allowBulkUnpublish && (buttonPermissions == null || buttonPermissions.canUnpublish)"
type="button"
button-style="link"
label="Unpublish"
@@ -104,7 +104,7 @@
</umb-button>
<umb-button
ng-if="options.allowBulkCopy"
ng-if="options.allowBulkCopy && (buttonPermissions == null || buttonPermissions.canCopy)"
type="button"
button-style="link"
label="Copy"
@@ -115,7 +115,7 @@
</umb-button>
<umb-button
ng-if="options.allowBulkMove"
ng-if="options.allowBulkMove && (buttonPermissions == null || buttonPermissions.canMove)"
type="button"
button-style="link"
label="Move"
@@ -126,7 +126,7 @@
</umb-button>
<umb-button
ng-if="options.allowBulkDelete"
ng-if="options.allowBulkDelete && (buttonPermissions == null || buttonPermissions.canDelete)"
type="button"
button-style="link"
label="Delete"

View File

@@ -5,7 +5,8 @@ var app = angular.module('umbraco', [
'umbraco.services',
'umbraco.mocks',
'umbraco.security',
'ngCookies'
'ngCookies',
'LocalStorageModule'
]);
/* For Angular 1.2: we need to load in Routing separately

View File

@@ -26,7 +26,7 @@ module.exports = function(karma) {
'lib/../build/belle/lib/underscore/underscore-min.js',
'lib/umbraco/Extensions.js',
'lib/../build/belle/lib/rgrove-lazyload/lazyload.js',
'lib/../build/belle/lib/angular-local-storage/angular-local-storage.min.js',
'test/config/app.unit.js',
'src/common/mocks/umbraco.servervariables.js',

View File

@@ -0,0 +1,48 @@
describe('list view helper tests', function () {
var $scope, listViewHelper;
beforeEach(module('LocalStorageModule'));
beforeEach(module('umbraco.services'));
beforeEach(inject(function ($injector) {
$scope = $injector.get('$rootScope');
listViewHelper = $injector.get('listViewHelper');
}));
describe('getButtonPermissions', function () {
it('should update the currentIdsWithPermissions dictionary', function () {
var currentIdsWithPermissions = {};
var result = listViewHelper.getButtonPermissions({ "1234": ["A", "B", "C"] }, currentIdsWithPermissions);
expect(_.has(currentIdsWithPermissions, "1234")).toBe(true);
expect(_.keys(currentIdsWithPermissions).length).toEqual(1);
});
it('returns button permissions', function () {
var currentIdsWithPermissions = {};
var result1 = listViewHelper.getButtonPermissions({ "1234": ["O", "C", "D", "M", "U"] }, currentIdsWithPermissions);
expect(result1["canCopy"]).toBe(true);
expect(result1["canCreate"]).toBe(true);
expect(result1["canDelete"]).toBe(true);
expect(result1["canMove"]).toBe(true);
expect(result1["canPublish"]).toBe(true);
expect(result1["canUnpublish"]).toBe(true);
var result2 = listViewHelper.getButtonPermissions({ "1234": ["A", "B"] }, currentIdsWithPermissions);
expect(result2["canCopy"]).toBe(false);
expect(result2["canCreate"]).toBe(false);
expect(result2["canDelete"]).toBe(false);
expect(result2["canMove"]).toBe(false);
expect(result2["canPublish"]).toBe(false);
expect(result2["canUnpublish"]).toBe(false);
});
});
});

View File

@@ -8,6 +8,7 @@ using System.Net.Http.Formatting;
using System.Text;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.ModelBinding.Binders;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Logging;
@@ -215,6 +216,19 @@ namespace Umbraco.Web.Editors
return HasPermission(permissionToCheck, nodeId);
}
/// <summary>
/// Returns permissions for all nodes passed in for the current user
/// </summary>
/// <param name="nodeIds"></param>
/// <returns></returns>
[HttpPost]
public Dictionary<int, string[]> GetPermissions(int[] nodeIds)
{
return Services.UserService
.GetPermissions(Security.CurrentUser, nodeIds)
.ToDictionary(x => x.EntityId, x => x.AssignedPermissions);
}
[HttpGet]
public bool HasPermission(string permissionToCheck, int nodeId)
{
@@ -352,9 +366,7 @@ namespace Umbraco.Web.Editors
return display;
}
/// <summary>
/// Publishes a document with a given ID
/// </summary>

View File

@@ -292,35 +292,9 @@ namespace Umbraco.Web.Models.Mapping
source.HasIdentity ? source.Id : source.ParentId)
.FirstOrDefault();
if (permissions == null)
{
return Enumerable.Empty<char>();
}
var result = new List<char>();
//can they publish ?
if (permissions.AssignedPermissions.Contains(ActionPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)))
{
result.Add(ActionPublish.Instance.Letter);
}
//can they send to publish ?
if (permissions.AssignedPermissions.Contains(ActionToPublish.Instance.Letter.ToString(CultureInfo.InvariantCulture)))
{
result.Add(ActionToPublish.Instance.Letter);
}
//can they save ?
if (permissions.AssignedPermissions.Contains(ActionUpdate.Instance.Letter.ToString(CultureInfo.InvariantCulture)))
{
result.Add(ActionUpdate.Instance.Letter);
}
//can they create ?
if (permissions.AssignedPermissions.Contains(ActionNew.Instance.Letter.ToString(CultureInfo.InvariantCulture)))
{
result.Add(ActionNew.Instance.Letter);
}
return result;
return permissions == null
? Enumerable.Empty<char>()
: permissions.AssignedPermissions.Where(x => x.Length == 1).Select(x => x.ToUpperInvariant()[0]);
}
}