diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 2fe1bb4d5b..a1e7c9955e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1741,7 +1741,15 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// - public void EmptyRecycleBin() + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] + public void EmptyRecycleBin() => EmptyRecycleBin(0); + + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + public void EmptyRecycleBin(int userId = 0) { using (new WriteLock(Locker)) { @@ -1771,7 +1779,7 @@ namespace Umbraco.Core.Services recycleBinEventArgs.RecycleBinEmptiedSuccessfully = success; uow.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); - Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + Audit(uow, AuditType.Delete, "Empty Content Recycle Bin performed by user", userId, Constants.System.RecycleBinContent); uow.Commit(); } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 7512064ed3..5a626c5d36 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -440,8 +440,16 @@ namespace Umbraco.Core.Services /// /// Empties the Recycle Bin by deleting all that resides in the bin /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use EmptyRecycleBin with explicit indication of user ID instead")] void EmptyRecycleBin(); + /// + /// Empties the Recycle Bin by deleting all that resides in the bin + /// + /// Optional Id of the User emptying the Recycle Bin + void EmptyRecycleBin(int userId = 0); + /// /// Rollback an object to a previous version. /// This will create a new version, which is a copy of all the old data. diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 3d1e9d321c..b8d9c8bcb6 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1227 +1,1227 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using AutoMapper; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Publishing; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Binders; -using Umbraco.Web.WebApi.Filters; -using Umbraco.Core.Events; -using Constants = Umbraco.Core.Constants; -using umbraco.cms.businesslogic; -using System.Collections; -using umbraco; - -namespace Umbraco.Web.Editors -{ - /// - /// The API controller used for editing content - /// - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the content application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] - public class ContentController : ContentControllerBase - { - /// - /// Constructor - /// - public ContentController() - : this(UmbracoContext.Current) - { - } - - /// - /// Constructor - /// - /// - public ContentController(UmbracoContext umbracoContext) - : base(umbracoContext) - { - } - - /// - /// Configures this controller with a custom action selector - /// - private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) - )); - } - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); - } - - /// - /// Updates the permissions for a content item for a particular user group - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to update - /// - [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) - { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be alowed to set granular permissions? - - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) - .ToDictionary(x => x.UserGroupId, x => x); - - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); - - //loop through each user group - foreach (var userGroup in allUserGroups) - { - //check if there's a permission set posted up for this user group - IEnumerable groupPermissions; - if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) - { - //create a string collection of the assigned letters - var groupPermissionCodes = groupPermissions.ToArray(); - - //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions - //for this group/node which will go back to the defaults - if (groupPermissionCodes.Length == 0) - { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored - else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) - { - //only remove them if they are actually currently assigned - if (contentPermissions.ContainsKey(userGroup.Id)) - { - //remove these permissions from this node for this group since the ones being assigned are the same as the defaults - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); - } - } - //if they are different we need to update, otherwise there's nothing to update - else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) - { - - Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); - } - } - } - - return GetDetailedPermissions(content, allUserGroups); - } - - /// - /// Returns the user group permissions for user groups assigned to this node - /// - /// - /// - /// - /// Permission check is done for letter 'R' which is for which the user must have access to to view - /// - [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - //TODO: Should non-admins be able to see detailed permissions? - - var allUserGroups = Services.UserService.GetAllUserGroups(); - - return GetDetailedPermissions(content, allUserGroups); - } - - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) - { - //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. - //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. - - var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); - - var defaultPermissionsAsDictionary = defaultPermissionsByGroup - .ToDictionary(x => Convert.ToInt32(x.Id), x => x); - - //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); - - //iterate over assigned and update the defaults with the real values - foreach (var assignedGroupPermission in assignedPermissionsByGroup) - { - var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; - - //clone the default permissions model to the assigned ones - defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); - - //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions - //and we'll re-check it if it's one of the explicitly assigned ones - foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) - { - permission.Checked = false; - permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); - } - - } - - return defaultPermissionsByGroup; - } - - /// - /// Returns an item to be used to display the recycle bin for content - /// - /// - public ContentItemDisplay GetRecycleBin() - { - var display = new ContentItemDisplay - { - Id = Constants.System.RecycleBinContent, - Alias = "recycleBin", - ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), - ContentTypeAlias = "recycleBin", - CreateDate = DateTime.Now, - IsContainer = true, - Path = "-1," + Constants.System.RecycleBinContent - }; - - TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); - - return display; - } - - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - SetupBlueprint(content, foundContent); - - return content; - } - - private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) - { - content.AllowPreview = false; - - //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); - - content.AllowedActions = new[] { "A" }; - content.IsBlueprint = true; - - var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; - var propsTab = content.Tabs.Last(); - propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - } - - /// - /// Gets the content json for the content id - /// - /// - /// If set to true, user and group start node permissions will be ignored. - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id, [FromUri]bool ignoreUserStartNodes = false) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetWithTreeDefinition(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - return content; - } - - /// - /// Gets an empty content item for the - /// - /// - /// - /// - /// If this is a container type, we'll remove the umbContainerView tab for a new item since - /// it cannot actually list children if it doesn't exist yet. - /// - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) - { - var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); - var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); - // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); - // if your user type doesn't have access to the Settings section it would not get this property mapped - if(mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - [OutgoingEditorModelEvent] - public ContentItemDisplay GetEmpty(int blueprintId, int parentId) - { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); - if (blueprint == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - blueprint.Id = 0; - blueprint.Name = string.Empty; - blueprint.ParentId = parentId; - - var mapped = Mapper.Map(blueprint); - - //remove this tab if it exists: umbContainerView - var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); - mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - return mapped; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(int id) - { - var url = Umbraco.NiceUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Guid id) - { - var url = Umbraco.UrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Gets the Url for a given node ID - /// - /// - /// - public HttpResponseMessage GetNiceUrl(Udi id) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetNiceUrl(guidUdi.Guid); - } - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); - } - - /// - /// Gets the children for the content id passed in - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren( - int id, - string includeProperties, - int pageNumber = 0, //TODO: This should be '1' as it's not the index - int pageSize = 0, - string orderBy = "SortOrder", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "") - { - long totalChildren; - IContent[] children; - if (pageNumber > 0 && pageSize > 0) - { - children = Services.ContentService - .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren - , orderBy, orderDirection, orderBySystemField, filter).ToArray(); - } - else - { - children = Services.ContentService.GetChildren(id).ToArray(); - totalChildren = children.Length; - } - - if (totalChildren == 0) - { - return new PagedResult>(0, 0, 0); - } - - var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); - pagedResult.Items = children.Select(content => - Mapper.Map>(content, - opts => - { - // if there's a list of property aliases to map - we will make sure to store this in the mapping context. - if (String.IsNullOrWhiteSpace(includeProperties) == false) - { - opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); - } - })); - - return pagedResult; - } - - [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] - public bool GetHasPermission(string permissionToCheck, int nodeId) - { - return HasPermission(permissionToCheck, nodeId); - } - - /// - /// Returns permissions for all nodes passed in for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - [HttpPost] - public Dictionary GetPermissions(int[] nodeIds) - { - var permissions = Services.UserService - .GetPermissions(Security.CurrentUser, nodeIds); - - var permissionsDictionary = new Dictionary(); - foreach (var nodeId in nodeIds) - { - var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); - permissionsDictionary.Add(nodeId, aggregatePerms); - } - - return permissionsDictionary; - } - - /// - /// Checks a nodes permission for the current user - /// TODO: This should be moved to the CurrentUserController? - /// - /// - /// - /// - [HttpGet] - public bool HasPermission(string permissionToCheck, int nodeId) - { - var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); - if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) - { - return true; - } - - return false; - } - - /// - /// Creates a blueprint from a content item - /// - /// The content id to copy - /// The name of the blueprint - /// - [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) - { - if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); - - var content = Services.ContentService.GetById(contentId); - if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - EnsureUniqueName(name, content, "name"); - - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); - - Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); - - var notificationModel = new SimpleNotificationModel(); - notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) - ); - - return notificationModel; - } - - private void EnsureUniqueName(string name, IContent content, string modelName) - { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); - if (existing.Any(x => x.Name == name && x.Id != content.Id)) - { - ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); - throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); - } - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - public ContentItemDisplay PostSaveBlueprint( - [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, - content => - { - EnsureUniqueName(content.Name, content, "Name"); - - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants - return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave( - [ModelBinder(typeof(ContentItemBinder))] - ContentItemSave contentItem) - { - return PostSaveInternal(contentItem, - content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) - { - //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. - //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all - //uploaded files to being *only* the actual file name (as it should be). - if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) - { - foreach (var file in contentItem.UploadedFiles) - { - file.FileName = Path.GetFileName(file.FileName); - } - } - - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - MapPropertyValues(contentItem); - - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this - if (ModelState.IsValid == false) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw a validation message - var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - - } - - //if the model state is not valid we cannot publish so change it to save - switch (contentItem.Action) - { - case ContentSaveAction.Publish: - contentItem.Action = ContentSaveAction.Save; - break; - case ContentSaveAction.PublishNew: - contentItem.Action = ContentSaveAction.SaveNew; - break; - } - } - - //initialize this to successful - var publishStatus = Attempt.Succeed(); - var wasCancelled = false; - - if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) - { - //save the item - var saveResult = saveMethod(contentItem.PersistedContent); - - wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; - } - else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) - { - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = sendResult == false; - } - else - { - //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; - } - - //return the updated model - var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); - - //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - //put the correct msgs in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - contentItem.ReleaseDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentSavedText") - ); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.SendPublish: - case ContentSaveAction.SendPublishNew: - if (wasCancelled == false) - { - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); - } - else - { - AddCancelMessage(display); - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); - break; - } - - //If the item is new and the operation was cancelled, we need to return a different - // status code so the UI can handle it since it won't be able to redirect since there - // is no Id to redirect to! - if (wasCancelled && IsCreatingAction(contentItem.Action)) - { - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); - } - - display.PersistedContent = contentItem.PersistedContent; - - return display; - } - - /// - /// Publishes a document with a given ID - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Publish access to this node. - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); - return Request.CreateValidationErrorResponse(notificationModel); - } - - //return ok - return Request.CreateResponse(HttpStatusCode.OK); - - } - - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) - { - var found = Services.ContentService.GetBlueprintById(id); - - if (found == null) - { - return HandleContentNotFound(id, false); - } - - Services.ContentService.DeleteBlueprint(found); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Moves an item to the recycle bin, if it is already there then it will permanently delete it - /// - /// - /// - /// - /// The CanAccessContentAuthorize attribute will deny access to this method if the current user - /// does not have Delete access to this node. - /// - [EnsureUserPermissionForContent("id", 'D')] - [HttpDelete] - [HttpPost] - public HttpResponseMessage DeleteById(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - { - return HandleContentNotFound(id, false); - } - - //if the current item is in the recycle bin - if (foundContent.IsInRecycleBin() == false) - { - var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); - if (moveResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - else - { - var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); - if (deleteResult == false) - { - //returning an object of INotificationModel will ensure that any pending - // notification messages are added to the response. - return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); - } - } - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Empties the recycle bin - /// - /// - /// - /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin - /// - [HttpDelete] - [HttpPost] - [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] - public HttpResponseMessage EmptyRecycleBin() - { - Services.ContentService.EmptyRecycleBin(); - - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); - } - - /// - /// Change the sort order for content - /// - /// - /// - [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) - { - if (sorted == null) - { - return Request.CreateResponse(HttpStatusCode.NotFound); - } - - //if there's nothing to sort just return ok - if (sorted.IdSortOrder.Length == 0) - { - return Request.CreateResponse(HttpStatusCode.OK); - } - - try - { - var contentService = Services.ContentService; - - // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) - { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); - } - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception ex) - { - LogHelper.Error("Could not update content sort order", ex); - throw; - } - } - - /// - /// Change the sort order for media - /// - /// - /// - [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) - { - var toMove = ValidateMoveOrCopy(move); - - Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Copies a content item and places the copy as a child of a given parent Id - /// - /// - /// - [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) - { - var toCopy = ValidateMoveOrCopy(copy); - - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); - - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); - return response; - } - - /// - /// Unpublishes a node with a given Id and returns the unpublished entity - /// - /// - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); - - var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); - - if (unpublishResult == false) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); - return content; - } - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - - base.MapPropertyValues(contentItem); - } - - /// - /// Ensures the item can be moved/copied to the new location - /// - /// - /// - private IContent ValidateMoveOrCopy(MoveOrCopy model) - { - if (model == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentService = Services.ContentService; - var toMove = contentService.GetById(model.Id); - if (toMove == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - if (model.ParentId < 0) - { - //cannot move if the content item is not allowed at the root - if (toMove.ContentType.AllowedAsRoot == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); - } - } - else - { - var parent = contentService.GetById(model.ParentId); - if (parent == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //check if the item is allowed under this one - if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() - .Any(x => x.Value == toMove.ContentType.Id) == false) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); - } - - // Check on paths - if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) - { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); - } - } - - return toMove; - } - - private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) - { - switch (status.StatusType) - { - case PublishStatusType.Success: - case PublishStatusType.SuccessAlreadyPublished: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - expireDate.HasValue - ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText") - ); - break; - case PublishStatusType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishStatusType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); - break; - case PublishStatusType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - }).Trim()); - break; - case PublishStatusType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! - break; - case PublishStatusType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - default: - throw new IndexOutOfRangeException(); - } - } - - - /// - /// Performs a permissions check for the user to check if it has access to the node based on - /// start node and/or permissions for the node - /// - /// The storage to add the content item to so it can be reused - /// - /// - /// - /// - /// The content to lookup, if the contentItem is not specified - /// - /// Specifies the already resolved content item to check against - /// If set to true, user and group start node permissions will be ignored. - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null, - bool ignoreUserStartNodes = false) - { - if (storage == null) throw new ArgumentNullException("storage"); - if (user == null) throw new ArgumentNullException("user"); - if (userService == null) throw new ArgumentNullException("userService"); - if (contentService == null) throw new ArgumentNullException("contentService"); - if (entityService == null) throw new ArgumentNullException("entityService"); - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - contentItem = contentService.GetById(nodeId); - //put the content item into storage so it can be retreived - // in the controller (saves a lookup) - storage[typeof(IContent).ToString()] = contentItem; - } - - if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - if(ignoreUserStartNodes) - { - return true; - } - - var hasPathAccess = (nodeId == Constants.System.Root) - ? user.HasContentRootAccess(entityService) - : (nodeId == Constants.System.RecycleBinContent) - ? user.HasContentBinAccess(entityService) - : user.HasPathAccess(contentItem, entityService); - - if (hasPathAccess == false) - { - return false; - } - - if (permissionsToCheck == null || permissionsToCheck.Length == 0) - { - return true; - } - - //get the implicit/inherited permissions for the user for this path, - //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) - var path = contentItem != null ? contentItem.Path : nodeId.ToString(); - var permission = userService.GetPermissionsForPath(user, path); - - var allowed = true; - foreach (var p in permissionsToCheck) - { - if (permission == null - || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) - { - allowed = false; - } - } - return allowed; - } - - [EnsureUserPermissionForContent("contentId", 'F')] - public IEnumerable GetNotificationOptions(int contentId) - { - var notifications = new List(); - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - - var actionList = ActionsResolver.Current.Actions; - foreach (var a in actionList) - { - if (a.ShowInNotifier) - { - NotifySetting n = new NotifySetting - { - Name = ui.Text("actions", a.Alias), - Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), - NotifyCode = a.Letter.ToString() - }; - notifications.Add(n); - } - } - return notifications; - } - - public void PostNotificationOptions(int contentId, string notifyOptions = "") - { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var node = new CMSNode(contentId); - - global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.ModelBinding; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Publishing; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Binders; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Events; +using Constants = Umbraco.Core.Constants; +using umbraco.cms.businesslogic; +using System.Collections; +using umbraco; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for editing content + /// + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the content application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] + [ContentControllerConfiguration] + public class ContentController : ContentControllerBase + { + /// + /// Constructor + /// + public ContentController() + : this(UmbracoContext.Current) + { + } + + /// + /// Constructor + /// + /// + public ContentController(UmbracoContext umbracoContext) + : base(umbracoContext) + { + } + + /// + /// Configures this controller with a custom action selector + /// + private class ContentControllerConfigurationAttribute : Attribute, IControllerConfiguration + { + public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetNiceUrl", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)) + )); + } + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(content => AutoMapperExtensions.MapWithUmbracoContext(content, UmbracoContext)); + } + + /// + /// Updates the permissions for a content item for a particular user group + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to update + /// + [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] + public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + { + if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be alowed to set granular permissions? + + var content = Services.ContentService.GetById(saveModel.ContentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //current permissions explicitly assigned to this content item + var contentPermissions = Services.ContentService.GetPermissionsForEntity(content) + .ToDictionary(x => x.UserGroupId, x => x); + + var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + + //loop through each user group + foreach (var userGroup in allUserGroups) + { + //check if there's a permission set posted up for this user group + IEnumerable groupPermissions; + if (saveModel.AssignedPermissions.TryGetValue(userGroup.Id, out groupPermissions)) + { + //create a string collection of the assigned letters + var groupPermissionCodes = groupPermissions.ToArray(); + + //check if there are no permissions assigned for this group save model, if that is the case we want to reset the permissions + //for this group/node which will go back to the defaults + if (groupPermissionCodes.Length == 0) + { + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + //check if they are the defaults, if so we should just remove them if they exist since it's more overhead having them stored + else if (userGroup.Permissions.UnsortedSequenceEqual(groupPermissionCodes)) + { + //only remove them if they are actually currently assigned + if (contentPermissions.ContainsKey(userGroup.Id)) + { + //remove these permissions from this node for this group since the ones being assigned are the same as the defaults + Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + } + } + //if they are different we need to update, otherwise there's nothing to update + else if (contentPermissions.ContainsKey(userGroup.Id) == false || contentPermissions[userGroup.Id].AssignedPermissions.UnsortedSequenceEqual(groupPermissionCodes) == false) + { + + Services.UserService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); + } + } + } + + return GetDetailedPermissions(content, allUserGroups); + } + + /// + /// Returns the user group permissions for user groups assigned to this node + /// + /// + /// + /// + /// Permission check is done for letter 'R' which is for which the user must have access to to view + /// + [EnsureUserPermissionForContent("contentId", 'R')] + public IEnumerable GetDetailedPermissions(int contentId) + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + //TODO: Should non-admins be able to see detailed permissions? + + var allUserGroups = Services.UserService.GetAllUserGroups(); + + return GetDetailedPermissions(content, allUserGroups); + } + + private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + { + //get all user groups and map their default permissions to the AssignedUserGroupPermissions model. + //we do this because not all groups will have true assigned permissions for this node so if they don't have assigned permissions, we need to show the defaults. + + var defaultPermissionsByGroup = Mapper.Map>(allUserGroups).ToArray(); + + var defaultPermissionsAsDictionary = defaultPermissionsByGroup + .ToDictionary(x => Convert.ToInt32(x.Id), x => x); + + //get the actual assigned permissions + var assignedPermissionsByGroup = Services.ContentService.GetPermissionsForEntity(content).ToArray(); + + //iterate over assigned and update the defaults with the real values + foreach (var assignedGroupPermission in assignedPermissionsByGroup) + { + var defaultUserGroupPermissions = defaultPermissionsAsDictionary[assignedGroupPermission.UserGroupId]; + + //clone the default permissions model to the assigned ones + defaultUserGroupPermissions.AssignedPermissions = AssignedUserGroupPermissions.ClonePermissions(defaultUserGroupPermissions.DefaultPermissions); + + //since there is custom permissions assigned to this node for this group, we need to clear all of the default permissions + //and we'll re-check it if it's one of the explicitly assigned ones + foreach (var permission in defaultUserGroupPermissions.AssignedPermissions.SelectMany(x => x.Value)) + { + permission.Checked = false; + permission.Checked = assignedGroupPermission.AssignedPermissions.Contains(permission.PermissionCode, StringComparer.InvariantCulture); + } + + } + + return defaultPermissionsByGroup; + } + + /// + /// Returns an item to be used to display the recycle bin for content + /// + /// + public ContentItemDisplay GetRecycleBin() + { + var display = new ContentItemDisplay + { + Id = Constants.System.RecycleBinContent, + Alias = "recycleBin", + ParentId = -1, + Name = Services.TextService.Localize("general/recycleBin"), + ContentTypeAlias = "recycleBin", + CreateDate = DateTime.Now, + IsContainer = true, + Path = "-1," + Constants.System.RecycleBinContent + }; + + TabsAndPropertiesResolver.AddListView(display, "content", Services.DataTypeService, Services.TextService); + + return display; + } + + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + + //set a custom path since the tree that renders this has the content type id as the parent + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); + + content.AllowedActions = new[] { "A" }; + content.IsBlueprint = true; + + var excludeProps = new[] { "_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template" }; + var propsTab = content.Tabs.Last(); + propsTab.Properties = propsTab.Properties + .Where(p => excludeProps.Contains(p.Alias) == false); + } + + /// + /// Gets the content json for the content id + /// + /// + /// If set to true, user and group start node permissions will be ignored. + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id, [FromUri]bool ignoreUserStartNodes = false) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetWithTreeDefinition(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + return content; + } + + /// + /// Gets an empty content item for the + /// + /// + /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) + { + var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.CreateContent("", parentId, contentType.Alias, UmbracoUser.Id); + var mapped = AutoMapperExtensions.MapWithUmbracoContext(emptyContent, UmbracoContext); + // translate the content type name if applicable + mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if(mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmpty(int blueprintId, int parentId) + { + var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + if (blueprint == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + blueprint.Id = 0; + blueprint.Name = string.Empty; + blueprint.ParentId = parentId; + + var mapped = Mapper.Map(blueprint); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(int id) + { + var url = Umbraco.NiceUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Guid id) + { + var url = Umbraco.UrlProvider.GetUrl(id); + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(url, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Gets the Url for a given node ID + /// + /// + /// + public HttpResponseMessage GetNiceUrl(Udi id) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetNiceUrl(guidUdi.Guid); + } + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + return GetChildren(id, null, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + /// + /// Gets the children for the content id passed in + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable>), "Items")] + public PagedResult> GetChildren( + int id, + string includeProperties, + int pageNumber = 0, //TODO: This should be '1' as it's not the index + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + long totalChildren; + IContent[] children; + if (pageNumber > 0 && pageSize > 0) + { + children = Services.ContentService + .GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren + , orderBy, orderDirection, orderBySystemField, filter).ToArray(); + } + else + { + children = Services.ContentService.GetChildren(id).ToArray(); + totalChildren = children.Length; + } + + if (totalChildren == 0) + { + return new PagedResult>(0, 0, 0); + } + + var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); + pagedResult.Items = children.Select(content => + Mapper.Map>(content, + opts => + { + // if there's a list of property aliases to map - we will make sure to store this in the mapping context. + if (String.IsNullOrWhiteSpace(includeProperties) == false) + { + opts.Items["IncludeProperties"] = includeProperties.Split(new[] { ", ", "," }, StringSplitOptions.RemoveEmptyEntries); + } + })); + + return pagedResult; + } + + [Obsolete("Dont use this, it is incorrectly named, use HasPermission instead")] + public bool GetHasPermission(string permissionToCheck, int nodeId) + { + return HasPermission(permissionToCheck, nodeId); + } + + /// + /// Returns permissions for all nodes passed in for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + [HttpPost] + public Dictionary GetPermissions(int[] nodeIds) + { + var permissions = Services.UserService + .GetPermissions(Security.CurrentUser, nodeIds); + + var permissionsDictionary = new Dictionary(); + foreach (var nodeId in nodeIds) + { + var aggregatePerms = permissions.GetAllPermissions(nodeId).ToArray(); + permissionsDictionary.Add(nodeId, aggregatePerms); + } + + return permissionsDictionary; + } + + /// + /// Checks a nodes permission for the current user + /// TODO: This should be moved to the CurrentUserController? + /// + /// + /// + /// + [HttpGet] + public bool HasPermission(string permissionToCheck, int nodeId) + { + var p = Services.UserService.GetPermissions(Security.CurrentUser, nodeId).GetAllPermissions(); + if (p.Contains(permissionToCheck.ToString(CultureInfo.InvariantCulture))) + { + return true; + } + + return false; + } + + /// + /// Creates a blueprint from a content item + /// + /// The content id to copy + /// The name of the blueprint + /// + [HttpPost] + public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name"); + + var content = Services.ContentService.GetById(contentId); + if (content == null) + throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + EnsureUniqueName(name, content, "name"); + + var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.CurrentUser.Id); + + Services.ContentService.SaveBlueprint(blueprint, Security.CurrentUser.Id); + + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddSuccessNotification( + Services.TextService.Localize("blueprints/createdBlueprintHeading"), + Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + ); + + return notificationModel; + } + + private void EnsureUniqueName(string name, IContent content, string modelName) + { + var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + if (existing.Any(x => x.Name == name && x.Id != content.Id)) + { + ModelState.AddModelError(modelName, Services.TextService.Localize("blueprints/duplicateBlueprintMessage")); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + public ContentItemDisplay PostSaveBlueprint( + [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, + content => + { + EnsureUniqueName(content.Name, content, "Name"); + + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants + return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave( + [ModelBinder(typeof(ContentItemBinder))] + ContentItemSave contentItem) + { + return PostSaveInternal(contentItem, + content => Services.ContentService.WithResult().Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func> saveMethod) + { + //Recent versions of IE/Edge may send in the full clientside file path instead of just the file name. + //To ensure similar behavior across all browsers no matter what they do - we strip the FileName property of all + //uploaded files to being *only* the actual file name (as it should be). + if (contentItem.UploadedFiles != null && contentItem.UploadedFiles.Any()) + { + foreach (var file in contentItem.UploadedFiles) + { + file.FileName = Path.GetFileName(file.FileName); + } + } + + //If we've reached here it means: + // * Our model has been bound + // * and validated + // * any file attachments have been saved to their temporary location for us to use + // * we have a reference to the DTO object and the persisted object + // * Permissions are valid + MapPropertyValues(contentItem); + + //We need to manually check the validation results here because: + // * We still need to save the entity even if there are validation value errors + // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) + // then we cannot continue saving, we can only display errors + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // a message indicating this + if (ModelState.IsValid == false) + { + if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && IsCreatingAction(contentItem.Action)) + { + //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! + // add the modelstate to the outgoing object and throw a validation message + var forDisplay = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + + } + + //if the model state is not valid we cannot publish so change it to save + switch (contentItem.Action) + { + case ContentSaveAction.Publish: + contentItem.Action = ContentSaveAction.Save; + break; + case ContentSaveAction.PublishNew: + contentItem.Action = ContentSaveAction.SaveNew; + break; + } + } + + //initialize this to successful + var publishStatus = Attempt.Succeed(); + var wasCancelled = false; + + if (contentItem.Action == ContentSaveAction.Save || contentItem.Action == ContentSaveAction.SaveNew) + { + //save the item + var saveResult = saveMethod(contentItem.PersistedContent); + + wasCancelled = saveResult.Success == false && saveResult.Result.StatusType == OperationStatusType.FailedCancelledByEvent; + } + else if (contentItem.Action == ContentSaveAction.SendPublish || contentItem.Action == ContentSaveAction.SendPublishNew) + { + var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = sendResult == false; + } + else + { + //publish the item and check if it worked, if not we will show a diff msg below + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result.StatusType == PublishStatusType.FailedCancelledByEvent; + } + + //return the updated model + var display = AutoMapperExtensions.MapWithUmbracoContext(contentItem.PersistedContent, UmbracoContext); + + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + //put the correct msgs in + switch (contentItem.Action) + { + case ContentSaveAction.Save: + case ContentSaveAction.SaveNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSavedHeader"), + contentItem.ReleaseDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentSavedWithReleaseDateText", new [] { contentItem.ReleaseDate.Value.ToLongDateString(), contentItem.ReleaseDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentSavedText") + ); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.SendPublish: + case ContentSaveAction.SendPublishNew: + if (wasCancelled == false) + { + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentSendToPublish"), + Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + } + else + { + AddCancelMessage(display); + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + ShowMessageForPublishStatus(publishStatus.Result, display, contentItem.ExpireDate); + break; + } + + //If the item is new and the operation was cancelled, we need to return a different + // status code so the UI can handle it since it won't be able to redirect since there + // is no Id to redirect to! + if (wasCancelled && IsCreatingAction(contentItem.Action)) + { + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + } + + display.PersistedContent = contentItem.PersistedContent; + + return display; + } + + /// + /// Publishes a document with a given ID + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Publish access to this node. + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + public HttpResponseMessage PostPublishById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + var publishResult = Services.ContentService.PublishWithStatus(foundContent, Security.CurrentUser.Id); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult.Result, notificationModel, foundContent.ExpireDate); + return Request.CreateValidationErrorResponse(notificationModel); + } + + //return ok + return Request.CreateResponse(HttpStatusCode.OK); + + } + + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteBlueprint(int id) + { + var found = Services.ContentService.GetBlueprintById(id); + + if (found == null) + { + return HandleContentNotFound(id, false); + } + + Services.ContentService.DeleteBlueprint(found); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Moves an item to the recycle bin, if it is already there then it will permanently delete it + /// + /// + /// + /// + /// The CanAccessContentAuthorize attribute will deny access to this method if the current user + /// does not have Delete access to this node. + /// + [EnsureUserPermissionForContent("id", 'D')] + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + { + return HandleContentNotFound(id, false); + } + + //if the current item is in the recycle bin + if (foundContent.IsInRecycleBin() == false) + { + var moveResult = Services.ContentService.WithResult().MoveToRecycleBin(foundContent, Security.CurrentUser.Id); + if (moveResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + else + { + var deleteResult = Services.ContentService.WithResult().Delete(foundContent, Security.CurrentUser.Id); + if (deleteResult == false) + { + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + } + } + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Empties the recycle bin + /// + /// + /// + /// attributed with EnsureUserPermissionForContent to verify the user has access to the recycle bin + /// + [HttpDelete] + [HttpPost] + [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, 'D')] + public HttpResponseMessage EmptyRecycleBin() + { + Services.ContentService.EmptyRecycleBin(Security.CurrentUser.Id); + + return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + } + + /// + /// Change the sort order for content + /// + /// + /// + [EnsureUserPermissionForContent("sorted.ParentId", 'S')] + public HttpResponseMessage PostSort(ContentSortOrder sorted) + { + if (sorted == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + //if there's nothing to sort just return ok + if (sorted.IdSortOrder.Length == 0) + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + try + { + var contentService = Services.ContentService; + + // Save content with new sort order and update content xml in db accordingly + if (contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id) == false) + { + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + } + return Request.CreateResponse(HttpStatusCode.OK); + } + catch (Exception ex) + { + LogHelper.Error("Could not update content sort order", ex); + throw; + } + } + + /// + /// Change the sort order for media + /// + /// + /// + [EnsureUserPermissionForContent("move.ParentId", 'M')] + public HttpResponseMessage PostMove(MoveOrCopy move) + { + var toMove = ValidateMoveOrCopy(move); + + Services.ContentService.Move(toMove, move.ParentId, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Copies a content item and places the copy as a child of a given parent Id + /// + /// + /// + [EnsureUserPermissionForContent("copy.ParentId", 'C')] + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + var toCopy = ValidateMoveOrCopy(copy); + + var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.CurrentUser.Id); + + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(c.Path, Encoding.UTF8, "application/json"); + return response; + } + + /// + /// Unpublishes a node with a given Id and returns the unpublished entity + /// + /// + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.WithResult().UnPublish(foundContent, Security.CurrentUser.Id); + + var content = AutoMapperExtensions.MapWithUmbracoContext(foundContent, UmbracoContext); + + if (unpublishResult == false) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + content.AddSuccessNotification(Services.TextService.Localize("content/unPublish"), Services.TextService.Localize("speechBubbles/contentUnpublished")); + return content; + } + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + + /// + /// Ensures the item can be moved/copied to the new location + /// + /// + /// + private IContent ValidateMoveOrCopy(MoveOrCopy model) + { + if (model == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentService = Services.ContentService; + var toMove = contentService.GetById(model.Id); + if (toMove == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + if (model.ParentId < 0) + { + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"))); + } + } + else + { + var parent = contentService.GetById(model.ParentId); + if (parent == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //check if the item is allowed under this one + if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() + .Any(x => x.Value == toMove.ContentType.Id) == false) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + } + + // Check on paths + if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new HttpResponseException( + Request.CreateNotificationValidationErrorResponse( + Services.TextService.Localize("moveOrCopy/notAllowedByPath"))); + } + } + + return toMove; + } + + private void ShowMessageForPublishStatus(PublishStatus status, INotificationModel display, DateTime? expireDate) + { + switch (status.StatusType) + { + case PublishStatusType.Success: + case PublishStatusType.SuccessAlreadyPublished: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + expireDate.HasValue + ? Services.TextService.Localize("speechBubbles/editContentPublishedWithExpireDateText", new [] { expireDate.Value.ToLongDateString(), expireDate.Value.ToShortTimeString() }) + : Services.TextService.Localize("speechBubbles/editContentPublishedText") + ); + break; + case PublishStatusType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) }).Trim()); + break; + case PublishStatusType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; + case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! + break; + case PublishStatusType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + default: + throw new IndexOutOfRangeException(); + } + } + + + /// + /// Performs a permissions check for the user to check if it has access to the node based on + /// start node and/or permissions for the node + /// + /// The storage to add the content item to so it can be reused + /// + /// + /// + /// + /// The content to lookup, if the contentItem is not specified + /// + /// Specifies the already resolved content item to check against + /// If set to true, user and group start node permissions will be ignored. + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null, + bool ignoreUserStartNodes = false) + { + if (storage == null) throw new ArgumentNullException("storage"); + if (user == null) throw new ArgumentNullException("user"); + if (userService == null) throw new ArgumentNullException("userService"); + if (contentService == null) throw new ArgumentNullException("contentService"); + if (entityService == null) throw new ArgumentNullException("entityService"); + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + contentItem = contentService.GetById(nodeId); + //put the content item into storage so it can be retreived + // in the controller (saves a lookup) + storage[typeof(IContent).ToString()] = contentItem; + } + + if (contentItem == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinContent) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + if(ignoreUserStartNodes) + { + return true; + } + + var hasPathAccess = (nodeId == Constants.System.Root) + ? user.HasContentRootAccess(entityService) + : (nodeId == Constants.System.RecycleBinContent) + ? user.HasContentBinAccess(entityService) + : user.HasPathAccess(contentItem, entityService); + + if (hasPathAccess == false) + { + return false; + } + + if (permissionsToCheck == null || permissionsToCheck.Length == 0) + { + return true; + } + + //get the implicit/inherited permissions for the user for this path, + //if there is no content item for this id, than just use the id as the path (i.e. -1 or -20) + var path = contentItem != null ? contentItem.Path : nodeId.ToString(); + var permission = userService.GetPermissionsForPath(user, path); + + var allowed = true; + foreach (var p in permissionsToCheck) + { + if (permission == null + || permission.GetAllPermissions().Contains(p.ToString(CultureInfo.InvariantCulture)) == false) + { + allowed = false; + } + } + return allowed; + } + + [EnsureUserPermissionForContent("contentId", 'F')] + public IEnumerable GetNotificationOptions(int contentId) + { + var notifications = new List(); + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var content = Services.ContentService.GetById(contentId); + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + + var actionList = ActionsResolver.Current.Actions; + foreach (var a in actionList) + { + if (a.ShowInNotifier) + { + NotifySetting n = new NotifySetting + { + Name = ui.Text("actions", a.Alias), + Checked = (UmbracoUser.GetNotifications(content.Path).IndexOf(a.Letter) > -1), + NotifyCode = a.Letter.ToString() + }; + notifications.Add(n); + } + } + return notifications; + } + + public void PostNotificationOptions(int contentId, string notifyOptions = "") + { + if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = Services.ContentService.GetById(contentId); + + if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var node = new CMSNode(contentId); + + global::umbraco.cms.businesslogic.workflow.Notification.UpdateNotifications(UmbracoUser, node, notifyOptions ?? ""); + } + } +}