From f35c63fa400c6e81e974a91d93916dc2135a8cc5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 31 Jul 2018 14:14:52 +0200 Subject: [PATCH] Renormalize --- .../umbraco_client/Dialogs/AssignDomain2.js | 290 +- src/Umbraco.Web/Editors/ContentController.cs | 2942 ++++++++--------- src/Umbraco.Web/Mvc/UrlHelperExtensions.cs | 158 +- src/Umbraco.Web/UrlHelperExtensions.cs | 386 +-- .../controls/Tree/CustomTreeService.cs | 308 +- .../umbraco/dialogs/AssignDomain2.aspx.cs | 166 +- .../umbraco/webservices/nodeSorter.asmx.cs | 524 +-- 7 files changed, 2387 insertions(+), 2387 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js index ddf38733e1..b5921b6576 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js @@ -1,145 +1,145 @@ -Umbraco.Sys.registerNamespace("Umbraco.Dialogs"); - -(function ($) { - - // register AssignDomain dialog - Umbraco.Dialogs.AssignDomain2 = base2.Base.extend({ - - _opts: null, - - _isRepeated: function (element) { - var inputs = $('form input.domain'); - var elementName = element.attr('name'); - var repeated = false; - inputs.each(function() { - var input = $(this); - if (input.attr('name') != elementName && input.val() == element.val()) - repeated = true; - }); - return repeated; - }, - - // constructor - constructor: function (opts) { - // merge options with default - this._opts = $.extend({ - invalidDomain: 'Invalid domain.', - duplicateDomain: 'Domain has already been assigned.' - }, opts); - }, - - // public methods/variables - - languages: null, - language: null, - domains: null, - - addDomain: function () { - this.domains.push({ - Name: "", - Lang: "" - }); - }, - - init: function () { - var self = this; - - self.domains = ko.observableArray(self._opts.domains); - self.languages = self._opts.languages; - self.language = self._opts.language; - self.removeDomain = function() { self.domains.remove(this); }; - - ko.applyBindings(self); - - $.validator.addMethod("domain", function (value, element, param) { - // beware! encode('test') == 'test-' - // read eg https://rt.cpan.org/Public/Bug/Display.html?id=94347 - value = punycode.encode(value); - // that regex is best-effort and certainly not exact - var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; - var isopt = this.optional(element); - var retest = re.test(value); - var ret = isopt || retest; - return ret; - }, self._opts.invalidDomain); - - function getDuplicateMessage(val, el) { - var other = $(el).nextAll('input').val(); - var msg = self._opts.duplicateDomain - if (other != "" && other != "!!!") - msg = msg + ' (' + other + ')'; - return msg; - } - - $.validator.addMethod("duplicate", function (value, element, param) { - return $(element).nextAll('input').val() == "" && !self._isRepeated($(element)); - }, getDuplicateMessage); - - $.validator.addClassRules({ - domain: { domain: true }, - duplicate: { duplicate: true } - }); - - $('form').validate({ - debug: true, - focusCleanup: true, - onkeyup: false - }); - - $('form input.domain').on('focus', function(event) { - if (event.type != 'focusin') return; - $(this).nextAll('input').val(""); - }); - - // force validation *now* - $('form').valid(); - - $('#btnSave').click(function () { - if (!$('form').valid()) - return false; - - var mask = $('#komask'); - var masked = mask.parent(); - mask.height(masked.height()); - mask.width(masked.width()); - mask.show(); - - var data = { nodeId: self._opts.nodeId, language: self.language ? self.language : 0, domains: self.domains }; - $.post(self._opts.restServiceLocation + 'PostSaveLanguageAndDomains', ko.toJSON(data), function (json) { - mask.hide(); - - if (json.Valid) { - UmbClientMgr.closeModalWindow(); - } - else { - var inputs = $('form input.domain'); - inputs.each(function() { $(this).nextAll('input').val(""); }); - for (var i = 0; i < json.Domains.length; i++) { - var d = json.Domains[i]; - if (d.Duplicate) - inputs.each(function() { - var input = $(this); - if (input.val() == d.Name) - input.nextAll('input').val(d.Other ? d.Other : "!!!"); - }); - } - $('form').valid(); - } - }) - .fail(function (xhr, textStatus, errorThrown) { - mask.css('opacity', 1).css('color', "#ff0000").html(xhr.responseText); - }); - return false; - }); - } - - }); - - // set defaults for jQuery ajax calls - $.ajaxSetup({ - dataType: 'json', - cache: false, - contentType: 'application/json; charset=utf-8' - }); - -})(jQuery); +Umbraco.Sys.registerNamespace("Umbraco.Dialogs"); + +(function ($) { + + // register AssignDomain dialog + Umbraco.Dialogs.AssignDomain2 = base2.Base.extend({ + + _opts: null, + + _isRepeated: function (element) { + var inputs = $('form input.domain'); + var elementName = element.attr('name'); + var repeated = false; + inputs.each(function() { + var input = $(this); + if (input.attr('name') != elementName && input.val() == element.val()) + repeated = true; + }); + return repeated; + }, + + // constructor + constructor: function (opts) { + // merge options with default + this._opts = $.extend({ + invalidDomain: 'Invalid domain.', + duplicateDomain: 'Domain has already been assigned.' + }, opts); + }, + + // public methods/variables + + languages: null, + language: null, + domains: null, + + addDomain: function () { + this.domains.push({ + Name: "", + Lang: "" + }); + }, + + init: function () { + var self = this; + + self.domains = ko.observableArray(self._opts.domains); + self.languages = self._opts.languages; + self.language = self._opts.language; + self.removeDomain = function() { self.domains.remove(this); }; + + ko.applyBindings(self); + + $.validator.addMethod("domain", function (value, element, param) { + // beware! encode('test') == 'test-' + // read eg https://rt.cpan.org/Public/Bug/Display.html?id=94347 + value = punycode.encode(value); + // that regex is best-effort and certainly not exact + var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*|-)?$/gi; + var isopt = this.optional(element); + var retest = re.test(value); + var ret = isopt || retest; + return ret; + }, self._opts.invalidDomain); + + function getDuplicateMessage(val, el) { + var other = $(el).nextAll('input').val(); + var msg = self._opts.duplicateDomain + if (other != "" && other != "!!!") + msg = msg + ' (' + other + ')'; + return msg; + } + + $.validator.addMethod("duplicate", function (value, element, param) { + return $(element).nextAll('input').val() == "" && !self._isRepeated($(element)); + }, getDuplicateMessage); + + $.validator.addClassRules({ + domain: { domain: true }, + duplicate: { duplicate: true } + }); + + $('form').validate({ + debug: true, + focusCleanup: true, + onkeyup: false + }); + + $('form input.domain').on('focus', function(event) { + if (event.type != 'focusin') return; + $(this).nextAll('input').val(""); + }); + + // force validation *now* + $('form').valid(); + + $('#btnSave').click(function () { + if (!$('form').valid()) + return false; + + var mask = $('#komask'); + var masked = mask.parent(); + mask.height(masked.height()); + mask.width(masked.width()); + mask.show(); + + var data = { nodeId: self._opts.nodeId, language: self.language ? self.language : 0, domains: self.domains }; + $.post(self._opts.restServiceLocation + 'PostSaveLanguageAndDomains', ko.toJSON(data), function (json) { + mask.hide(); + + if (json.Valid) { + UmbClientMgr.closeModalWindow(); + } + else { + var inputs = $('form input.domain'); + inputs.each(function() { $(this).nextAll('input').val(""); }); + for (var i = 0; i < json.Domains.length; i++) { + var d = json.Domains[i]; + if (d.Duplicate) + inputs.each(function() { + var input = $(this); + if (input.val() == d.Name) + input.nextAll('input').val(d.Other ? d.Other : "!!!"); + }); + } + $('form').valid(); + } + }) + .fail(function (xhr, textStatus, errorThrown) { + mask.css('opacity', 1).css('color', "#ff0000").html(xhr.responseText); + }); + return false; + }); + } + + }); + + // set defaults for jQuery ajax calls + $.ajaxSetup({ + dataType: 'json', + cache: false, + contentType: 'application/json; charset=utf-8' + }); + +})(jQuery); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 1c61d3910d..7b5b728e7d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1,1471 +1,1471 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -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.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.Persistence.Querying; -using Umbraco.Web.PublishedCache; -using Umbraco.Core.Events; -using Umbraco.Core.Models.Validation; -using Umbraco.Web.Models; -using Umbraco.Web.WebServices; -using Umbraco.Web._Legacy.Actions; -using Constants = Umbraco.Core.Constants; -using ContentVariation = Umbraco.Core.Models.ContentVariation; -using Language = Umbraco.Web.Models.ContentEditing.Language; - -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 - { - private readonly IPublishedSnapshotService _publishedSnapshotService; - - public ContentController(IPublishedSnapshotService publishedSnapshotService) - { - if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); - _publishedSnapshotService = publishedSnapshotService; - } - - /// - /// 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)) - )); - } - } - - /// - /// Returns true if any content types have culture variation enabled - /// - /// - [HttpGet] - [WebApi.UmbracoAuthorize, OverrideAuthorization] - public bool AllowsCultureVariation() - { - var contentTypes = Services.ContentTypeService.GetAll(); - return contentTypes.Any(contentType => contentType.VariesByCulture()); - } - - /// - /// Return content for the specified ids - /// - /// - /// - [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) - { - //fixme what about cultures? - - var foundContent = Services.ContentService.GetByIds(ids); - return foundContent.Select(x => MapToDisplay(x)); - } - - /// - /// 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.GetPermissions(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.GetPermissions(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; - } - - //fixme what about cultures? - public ContentItemDisplay GetBlueprintById(int id) - { - var foundContent = Services.ContentService.GetBlueprintById(id); - if (foundContent == null) - { - HandleContentNotFound(id); - } - - var content = MapToDisplay(foundContent); - - 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 - /// - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(int id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - return null;//irrelevant since the above throws - } - var content = MapToDisplay(foundContent, culture); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Guid id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - if (foundContent == null) - { - HandleContentNotFound(id); - return null;//irrelevant since the above throws - } - - var content = MapToDisplay(foundContent, culture); - return content; - } - - /// - /// Gets the content json for the content id - /// - /// - /// - [OutgoingEditorModelEvent] - [EnsureUserPermissionForContent("id")] - public ContentItemDisplay GetById(Udi id, string culture = null) - { - var guidUdi = id as GuidUdi; - if (guidUdi != null) - { - return GetById(guidUdi.Guid, culture); - } - - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - /// - /// 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.Get(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); - var mapped = MapToDisplay(emptyContent); - - //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 }); - - if (contentType.VariesByCulture()) - { - //Remove all variants except for the default since currently the default must be saved before other variants can be edited - //TODO: Allow for editing all variants at once ... this will be a future task - mapped.Variants = new[] { mapped.Variants.FirstOrDefault(x => x.IsCurrent) }; - } - - 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.Url(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) - { - IQuery queryFilter = null; - if (filter.IsNullOrWhiteSpace() == false) - { - //add the default text filter - queryFilter = SqlContext.Query() - .Where(x => x.Name.Contains(filter)); - } - - children = Services.ContentService - .GetPagedChildren( - id, (pageNumber - 1), pageSize, - out totalChildren, - orderBy, orderDirection, orderBySystemField, - queryFilter).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; - } - - /// - /// 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.GetUserId().ResultOr(0)); - - Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId().ResultOr(0)); - - 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(ContentItemBinder))] 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 OperationResult.Succeed(new EventMessages()); - }); - SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); - - return contentItemDisplay; - } - - /// - /// Saves content - /// - /// - [FileUploadCleanupFilter] - [ContentPostValidate] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) - { - var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); - //ensure the active culture is still selected - if (!contentItem.Culture.IsNullOrWhiteSpace()) - { - foreach (var contentVariation in contentItemDisplay.Variants) - { - contentVariation.IsCurrent = contentVariation.Language.IsoCode.InvariantEquals(contentItem.Culture); - } - } - return contentItemDisplay; - } - - private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) - { - //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 (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(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 = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); - 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 = new PublishResult(null, contentItem.PersistedContent); - 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 == OperationResultType.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 - { - PublishInternal(contentItem, ref publishStatus, ref wasCancelled); - } - - //get the updated model - var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); - - //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"), - 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, display); - 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; - } - - /// - /// Performs the publishing operation for a content item - /// - /// - /// - /// - /// - /// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal - /// - private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) - { - if (!contentItem.PersistedContent.ContentType.VariesByCulture()) - { - //its invariant, proceed normally - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); - wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; - } - else - { - var canPublish = true; - - //check if we are publishing other variants and validate them - var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase); - var otherVariantsToValidate = contentItem.PublishVariations.Where(x => !x.Culture.InvariantEquals(contentItem.Culture)).ToList(); - - //validate any mandatory variants that are not in the list - var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) - .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above - .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant - .Where(x => x.Mandatory); - foreach (var lang in mandatoryLangs) - { - //cannot continue publishing since a required language that is not currently being published isn't published - if (!contentItem.PersistedContent.IsCulturePublished(lang.IsoCode)) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentReqCulturePublishError", new[] { allLangs[lang.IsoCode].CultureName }); - ModelState.AddModelError("publish_variant_" + lang.IsoCode + "_", errMsg); - canPublish = false; - } - } - - if (canPublish) - { - //validate all other variants to be published - foreach (var publishVariation in otherVariantsToValidate) - { - //validate the content item and the culture property values, we don't need to validate any invariant property values here because they will have - //been validated in the post. - var valid = contentItem.PersistedContent.IsValid(publishVariation.Culture); - if (!valid) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentCultureValidationError", new[] { allLangs[publishVariation.Culture].CultureName }); - ModelState.AddModelError("publish_variant_" + publishVariation.Culture + "_", errMsg); - canPublish = false; - } - } - } - - if (canPublish) - { - //try to publish all the values on the model - canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); - } - - if (canPublish) - { - //proceed to publish if all validation still succeeds - publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); - wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; - } - else - { - //can only save - var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); - publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); - wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; - } - } - } - - /// - /// This will call TryPublishValues on the content item for each culture that needs to be published including the invariant culture - /// - /// - /// - /// - /// - private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) - { - var culturesToPublish = new List { contentItem.Culture }; - culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); - - foreach(var culture in culturesToPublish) - { - // publishing any culture, implies the invariant culture - var valid = contentItem.PersistedContent.PublishCulture(culture); - if (!valid) - { - var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); - ModelState.AddModelError("publish_variant_" + culture + "_", errMsg); - return false; - } - } - - return true; - } - - /// - /// 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.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); - if (publishResult.Success == false) - { - var notificationModel = new SimpleNotificationModel(); - ShowMessageForPublishStatus(publishResult, notificationModel); - 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.Trashed == false) - { - var moveResult = Services.ContentService.MoveToRecycleBin(foundContent, Security.GetUserId().ResultOr(0)); - if (moveResult.Success == 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.Delete(foundContent, Security.GetUserId().ResultOr(0)); - if (deleteResult.Success == 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)] - 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) == false) - { - Logger.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) - { - Logger.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.GetUserId().ResultOr(0)); - - 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.GetUserId().ResultOr(0)); - - 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 - /// - /// The content id to unpublish - /// The culture variant for the content id to unpublish, if none specified will unpublish all variants of the content - /// - [EnsureUserPermissionForContent("id", 'U')] - [OutgoingEditorModelEvent] - public ContentItemDisplay PostUnPublish(int id, string culture = null) - { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); - - if (foundContent == null) - HandleContentNotFound(id); - - var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0)); - - var content = MapToDisplay(foundContent, culture); - - if (!unpublishResult.Success) - { - AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); - } - else - { - //fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status - - content.AddSuccessNotification( - Services.TextService.Localize("content/unPublish"), - unpublishResult.Result == UnpublishResultType.SuccessCulture - ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) - : Services.TextService.Localize("speechBubbles/contentUnpublished")); - - return content; - } - } - - [HttpPost] - public DomainSave PostSaveLanguageAndDomains(DomainSave model) - { - var node = Services.ContentService.GetById(model.NodeId); - - if (node == null) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent($"There is no content node with id {model.NodeId}."); - response.ReasonPhrase = "Node Not Found."; - throw new HttpResponseException(response); - } - - var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); - - if (permission.AssignedPermissions.Contains(ActionAssignDomain.Instance.Letter.ToString(), StringComparer.Ordinal) == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("You do not have permission to assign domains on that node."); - response.ReasonPhrase = "Permission Denied."; - throw new HttpResponseException(response); - } - - model.Valid = true; - var domains = Services.DomainService.GetAssignedDomains(model.NodeId, true).ToArray(); - var languages = Services.LocalizationService.GetAllLanguages().ToArray(); - var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; - - // process wildcard - if (language != null) - { - // yet there is a race condition here... - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) - { - wildcard.LanguageId = language.Id; - } - else - { - wildcard = new UmbracoDomain("*" + model.NodeId) - { - LanguageId = model.Language, - RootContentId = model.NodeId - }; - } - - var saveAttempt = Services.DomainService.Save(wildcard); - if (saveAttempt == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("Saving domain failed"); - response.ReasonPhrase = saveAttempt.Result.Result.ToString(); - throw new HttpResponseException(response); - } - } - else - { - var wildcard = domains.FirstOrDefault(d => d.IsWildcard); - if (wildcard != null) - { - Services.DomainService.Delete(wildcard); - } - } - - // process domains - // delete every (non-wildcard) domain, that exists in the DB yet is not in the model - foreach (var domain in domains.Where(d => d.IsWildcard == false && model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))) - { - Services.DomainService.Delete(domain); - } - - var names = new List(); - - // create or update domains in the model - foreach (var domainModel in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) - { - language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); - if (language == null) - { - continue; - } - - var name = domainModel.Name.ToLowerInvariant(); - if (names.Contains(name)) - { - domainModel.Duplicate = true; - continue; - } - names.Add(name); - var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); - if (domain != null) - { - domain.LanguageId = language.Id; - Services.DomainService.Save(domain); - } - else if (Services.DomainService.Exists(domainModel.Name)) - { - domainModel.Duplicate = true; - var xdomain = Services.DomainService.GetByName(domainModel.Name); - var xrcid = xdomain.RootContentId; - if (xrcid.HasValue) - { - var xcontent = Services.ContentService.GetById(xrcid.Value); - var xnames = new List(); - while (xcontent != null) - { - xnames.Add(xcontent.Name); - if (xcontent.ParentId < -1) - xnames.Add("Recycle Bin"); - xcontent = xcontent.Parent(Services.ContentService); - } - xnames.Reverse(); - domainModel.Other = "/" + string.Join("/", xnames); - } - } - else - { - // yet there is a race condition here... - var newDomain = new UmbracoDomain(name) - { - LanguageId = domainModel.Lang, - RootContentId = model.NodeId - }; - var saveAttempt = Services.DomainService.Save(newDomain); - if (saveAttempt == false) - { - var response = Request.CreateResponse(HttpStatusCode.BadRequest); - response.Content = new StringContent("Saving new domain failed"); - response.ReasonPhrase = saveAttempt.Result.Result.ToString(); - throw new HttpResponseException(response); - } - } - } - - model.Valid = model.Domains.All(m => m.Duplicate == false); - - return model; - } - - /// - /// Maps the dto property values to the persisted model - /// - /// - private void MapPropertyValues(ContentItemSave contentItem) - { - //Don't update the name if it is empty - if (!contentItem.Name.IsNullOrWhiteSpace()) - { - if (contentItem.PersistedContent.ContentType.VariesByCulture()) - { - if (contentItem.Culture.IsNullOrWhiteSpace()) - throw new InvalidOperationException($"Cannot set culture name without a culture."); - contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); - } - else - { - contentItem.PersistedContent.Name = contentItem.Name; - } - } - - //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); - Logger.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; - } - } - - bool Varies(Property property) => property.PropertyType.VariesByCulture(); - - MapPropertyValues( - contentItem, - (save, property) => Varies(property) ? property.GetValue(save.Culture) : property.GetValue(), //get prop val - (save, property, v) => { if (Varies(property)) property.SetValue(v, save.Culture); else property.SetValue(v); }); //set prop val - } - - /// - /// 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(PublishResult status, INotificationModel display) - { - switch (status.Result) - { - case PublishResultType.Success: - case PublishResultType.SuccessAlready: - display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); - break; - case PublishResultType.FailedPathNotPublished: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); - break; - case PublishResultType.FailedCancelledByEvent: - AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); - break; - case PublishResultType.FailedAwaitingRelease: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); - break; - case PublishResultType.FailedHasExpired: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); - break; - case PublishResultType.FailedIsTrashed: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! - break; - case PublishResultType.FailedContentInvalid: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - $"{status.Content.Name} ({status.Content.Id})", - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); - break; - case PublishResultType.FailedByCulture: - display.AddWarningNotification( - Services.TextService.Localize("publish"), - "publish/contentPublishedFailedByCulture"); // fixme properly localize! - break; - default: - throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); - } - } - - /// - /// 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 - /// - internal static bool CheckPermissions( - IDictionary storage, - IUser user, - IUserService userService, - IContentService contentService, - IEntityService entityService, - int nodeId, - char[] permissionsToCheck = null, - IContent contentItem = null) - { - 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); - } - - 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; - } - - /// - /// Used to map an instance to a and ensuring a language is present if required - /// - /// - /// - /// - private ContentItemDisplay MapToDisplay(IContent content, string culture = null) - { - //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, - // the Cuture property of ContentItemDisplay must exist (at least currently). - if (culture == null && content.ContentType.VariesByCulture()) - { - //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. - culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); - } - - var display = ContextMapper.Map(content, - new Dictionary { { ContextMapper.CultureKey, culture } }); - - return display; - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +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.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.Persistence.Querying; +using Umbraco.Web.PublishedCache; +using Umbraco.Core.Events; +using Umbraco.Core.Models.Validation; +using Umbraco.Web.Models; +using Umbraco.Web.WebServices; +using Umbraco.Web._Legacy.Actions; +using Constants = Umbraco.Core.Constants; +using ContentVariation = Umbraco.Core.Models.ContentVariation; +using Language = Umbraco.Web.Models.ContentEditing.Language; + +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 + { + private readonly IPublishedSnapshotService _publishedSnapshotService; + + public ContentController(IPublishedSnapshotService publishedSnapshotService) + { + if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); + _publishedSnapshotService = publishedSnapshotService; + } + + /// + /// 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)) + )); + } + } + + /// + /// Returns true if any content types have culture variation enabled + /// + /// + [HttpGet] + [WebApi.UmbracoAuthorize, OverrideAuthorization] + public bool AllowsCultureVariation() + { + var contentTypes = Services.ContentTypeService.GetAll(); + return contentTypes.Any(contentType => contentType.VariesByCulture()); + } + + /// + /// Return content for the specified ids + /// + /// + /// + [FilterAllowedOutgoingContent(typeof(IEnumerable))] + public IEnumerable GetByIds([FromUri]int[] ids) + { + //fixme what about cultures? + + var foundContent = Services.ContentService.GetByIds(ids); + return foundContent.Select(x => MapToDisplay(x)); + } + + /// + /// 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.GetPermissions(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.GetPermissions(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; + } + + //fixme what about cultures? + public ContentItemDisplay GetBlueprintById(int id) + { + var foundContent = Services.ContentService.GetBlueprintById(id); + if (foundContent == null) + { + HandleContentNotFound(id); + } + + var content = MapToDisplay(foundContent); + + 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 + /// + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(int id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + return null;//irrelevant since the above throws + } + var content = MapToDisplay(foundContent, culture); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Guid id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + if (foundContent == null) + { + HandleContentNotFound(id); + return null;//irrelevant since the above throws + } + + var content = MapToDisplay(foundContent, culture); + return content; + } + + /// + /// Gets the content json for the content id + /// + /// + /// + [OutgoingEditorModelEvent] + [EnsureUserPermissionForContent("id")] + public ContentItemDisplay GetById(Udi id, string culture = null) + { + var guidUdi = id as GuidUdi; + if (guidUdi != null) + { + return GetById(guidUdi.Guid, culture); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + /// + /// 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.Get(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); + var mapped = MapToDisplay(emptyContent); + + //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 }); + + if (contentType.VariesByCulture()) + { + //Remove all variants except for the default since currently the default must be saved before other variants can be edited + //TODO: Allow for editing all variants at once ... this will be a future task + mapped.Variants = new[] { mapped.Variants.FirstOrDefault(x => x.IsCurrent) }; + } + + 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.Url(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) + { + IQuery queryFilter = null; + if (filter.IsNullOrWhiteSpace() == false) + { + //add the default text filter + queryFilter = SqlContext.Query() + .Where(x => x.Name.Contains(filter)); + } + + children = Services.ContentService + .GetPagedChildren( + id, (pageNumber - 1), pageSize, + out totalChildren, + orderBy, orderDirection, orderBySystemField, + queryFilter).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; + } + + /// + /// 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.GetUserId().ResultOr(0)); + + Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId().ResultOr(0)); + + 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(ContentItemBinder))] 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 OperationResult.Succeed(new EventMessages()); + }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; + } + + /// + /// Saves content + /// + /// + [FileUploadCleanupFilter] + [ContentPostValidate] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) + { + var contentItemDisplay = PostSaveInternal(contentItem, content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id)); + //ensure the active culture is still selected + if (!contentItem.Culture.IsNullOrWhiteSpace()) + { + foreach (var contentVariation in contentItemDisplay.Variants) + { + contentVariation.IsCurrent = contentVariation.Language.IsoCode.InvariantEquals(contentItem.Culture); + } + } + return contentItemDisplay; + } + + private ContentItemDisplay PostSaveInternal(ContentItemSave contentItem, Func saveMethod) + { + //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 (!RequiredForPersistenceAttribute.HasRequiredValuesForPersistence(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 = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); + 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 = new PublishResult(null, contentItem.PersistedContent); + 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 == OperationResultType.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 + { + PublishInternal(contentItem, ref publishStatus, ref wasCancelled); + } + + //get the updated model + var display = MapToDisplay(contentItem.PersistedContent, contentItem.Culture); + + //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"), + 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, display); + 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; + } + + /// + /// Performs the publishing operation for a content item + /// + /// + /// + /// + /// + /// If this is a culture variant than we need to do some validation, if it's not we'll publish as normal + /// + private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) + { + if (!contentItem.PersistedContent.ContentType.VariesByCulture()) + { + //its invariant, proceed normally + publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); + wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; + } + else + { + var canPublish = true; + + //check if we are publishing other variants and validate them + var allLangs = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase); + var otherVariantsToValidate = contentItem.PublishVariations.Where(x => !x.Culture.InvariantEquals(contentItem.Culture)).ToList(); + + //validate any mandatory variants that are not in the list + var mandatoryLangs = Mapper.Map, IEnumerable>(allLangs.Values) + .Where(x => otherVariantsToValidate.All(v => !v.Culture.InvariantEquals(x.IsoCode))) //don't include variants above + .Where(x => !x.IsoCode.InvariantEquals(contentItem.Culture)) //don't include the current variant + .Where(x => x.Mandatory); + foreach (var lang in mandatoryLangs) + { + //cannot continue publishing since a required language that is not currently being published isn't published + if (!contentItem.PersistedContent.IsCulturePublished(lang.IsoCode)) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentReqCulturePublishError", new[] { allLangs[lang.IsoCode].CultureName }); + ModelState.AddModelError("publish_variant_" + lang.IsoCode + "_", errMsg); + canPublish = false; + } + } + + if (canPublish) + { + //validate all other variants to be published + foreach (var publishVariation in otherVariantsToValidate) + { + //validate the content item and the culture property values, we don't need to validate any invariant property values here because they will have + //been validated in the post. + var valid = contentItem.PersistedContent.IsValid(publishVariation.Culture); + if (!valid) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentCultureValidationError", new[] { allLangs[publishVariation.Culture].CultureName }); + ModelState.AddModelError("publish_variant_" + publishVariation.Culture + "_", errMsg); + canPublish = false; + } + } + } + + if (canPublish) + { + //try to publish all the values on the model + canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); + } + + if (canPublish) + { + //proceed to publish if all validation still succeeds + publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); + wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; + } + else + { + //can only save + var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); + wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; + } + } + } + + /// + /// This will call TryPublishValues on the content item for each culture that needs to be published including the invariant culture + /// + /// + /// + /// + /// + private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) + { + var culturesToPublish = new List { contentItem.Culture }; + culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); + + foreach(var culture in culturesToPublish) + { + // publishing any culture, implies the invariant culture + var valid = contentItem.PersistedContent.PublishCulture(culture); + if (!valid) + { + var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); + ModelState.AddModelError("publish_variant_" + culture + "_", errMsg); + return false; + } + } + + return true; + } + + /// + /// 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.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); + if (publishResult.Success == false) + { + var notificationModel = new SimpleNotificationModel(); + ShowMessageForPublishStatus(publishResult, notificationModel); + 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.Trashed == false) + { + var moveResult = Services.ContentService.MoveToRecycleBin(foundContent, Security.GetUserId().ResultOr(0)); + if (moveResult.Success == 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.Delete(foundContent, Security.GetUserId().ResultOr(0)); + if (deleteResult.Success == 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)] + 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) == false) + { + Logger.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) + { + Logger.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.GetUserId().ResultOr(0)); + + 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.GetUserId().ResultOr(0)); + + 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 + /// + /// The content id to unpublish + /// The culture variant for the content id to unpublish, if none specified will unpublish all variants of the content + /// + [EnsureUserPermissionForContent("id", 'U')] + [OutgoingEditorModelEvent] + public ContentItemDisplay PostUnPublish(int id, string culture = null) + { + var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + + if (foundContent == null) + HandleContentNotFound(id); + + var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0)); + + var content = MapToDisplay(foundContent, culture); + + if (!unpublishResult.Success) + { + AddCancelMessage(content); + throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + } + else + { + //fixme should have a better localized method for when we have the UnpublishResultType.SuccessMandatoryCulture status + + content.AddSuccessNotification( + Services.TextService.Localize("content/unPublish"), + unpublishResult.Result == UnpublishResultType.SuccessCulture + ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) + : Services.TextService.Localize("speechBubbles/contentUnpublished")); + + return content; + } + } + + [HttpPost] + public DomainSave PostSaveLanguageAndDomains(DomainSave model) + { + var node = Services.ContentService.GetById(model.NodeId); + + if (node == null) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent($"There is no content node with id {model.NodeId}."); + response.ReasonPhrase = "Node Not Found."; + throw new HttpResponseException(response); + } + + var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); + + if (permission.AssignedPermissions.Contains(ActionAssignDomain.Instance.Letter.ToString(), StringComparer.Ordinal) == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("You do not have permission to assign domains on that node."); + response.ReasonPhrase = "Permission Denied."; + throw new HttpResponseException(response); + } + + model.Valid = true; + var domains = Services.DomainService.GetAssignedDomains(model.NodeId, true).ToArray(); + var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; + + // process wildcard + if (language != null) + { + // yet there is a race condition here... + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + { + wildcard.LanguageId = language.Id; + } + else + { + wildcard = new UmbracoDomain("*" + model.NodeId) + { + LanguageId = model.Language, + RootContentId = model.NodeId + }; + } + + var saveAttempt = Services.DomainService.Save(wildcard); + if (saveAttempt == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("Saving domain failed"); + response.ReasonPhrase = saveAttempt.Result.Result.ToString(); + throw new HttpResponseException(response); + } + } + else + { + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + { + Services.DomainService.Delete(wildcard); + } + } + + // process domains + // delete every (non-wildcard) domain, that exists in the DB yet is not in the model + foreach (var domain in domains.Where(d => d.IsWildcard == false && model.Domains.All(m => m.Name.InvariantEquals(d.DomainName) == false))) + { + Services.DomainService.Delete(domain); + } + + var names = new List(); + + // create or update domains in the model + foreach (var domainModel in model.Domains.Where(m => string.IsNullOrWhiteSpace(m.Name) == false)) + { + language = languages.FirstOrDefault(l => l.Id == domainModel.Lang); + if (language == null) + { + continue; + } + + var name = domainModel.Name.ToLowerInvariant(); + if (names.Contains(name)) + { + domainModel.Duplicate = true; + continue; + } + names.Add(name); + var domain = domains.FirstOrDefault(d => d.DomainName.InvariantEquals(domainModel.Name)); + if (domain != null) + { + domain.LanguageId = language.Id; + Services.DomainService.Save(domain); + } + else if (Services.DomainService.Exists(domainModel.Name)) + { + domainModel.Duplicate = true; + var xdomain = Services.DomainService.GetByName(domainModel.Name); + var xrcid = xdomain.RootContentId; + if (xrcid.HasValue) + { + var xcontent = Services.ContentService.GetById(xrcid.Value); + var xnames = new List(); + while (xcontent != null) + { + xnames.Add(xcontent.Name); + if (xcontent.ParentId < -1) + xnames.Add("Recycle Bin"); + xcontent = xcontent.Parent(Services.ContentService); + } + xnames.Reverse(); + domainModel.Other = "/" + string.Join("/", xnames); + } + } + else + { + // yet there is a race condition here... + var newDomain = new UmbracoDomain(name) + { + LanguageId = domainModel.Lang, + RootContentId = model.NodeId + }; + var saveAttempt = Services.DomainService.Save(newDomain); + if (saveAttempt == false) + { + var response = Request.CreateResponse(HttpStatusCode.BadRequest); + response.Content = new StringContent("Saving new domain failed"); + response.ReasonPhrase = saveAttempt.Result.Result.ToString(); + throw new HttpResponseException(response); + } + } + } + + model.Valid = model.Domains.All(m => m.Duplicate == false); + + return model; + } + + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + //Don't update the name if it is empty + if (!contentItem.Name.IsNullOrWhiteSpace()) + { + if (contentItem.PersistedContent.ContentType.VariesByCulture()) + { + if (contentItem.Culture.IsNullOrWhiteSpace()) + throw new InvalidOperationException($"Cannot set culture name without a culture."); + contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); + } + else + { + contentItem.PersistedContent.Name = contentItem.Name; + } + } + + //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); + Logger.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; + } + } + + bool Varies(Property property) => property.PropertyType.VariesByCulture(); + + MapPropertyValues( + contentItem, + (save, property) => Varies(property) ? property.GetValue(save.Culture) : property.GetValue(), //get prop val + (save, property, v) => { if (Varies(property)) property.SetValue(v, save.Culture); else property.SetValue(v); }); //set prop val + } + + /// + /// 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(PublishResult status, INotificationModel display) + { + switch (status.Result) + { + case PublishResultType.Success: + case PublishResultType.SuccessAlready: + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + Services.TextService.Localize("speechBubbles/editContentPublishedText")); + break; + case PublishResultType.FailedPathNotPublished: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedByParent", + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); + break; + case PublishResultType.FailedCancelledByEvent: + AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); + break; + case PublishResultType.FailedAwaitingRelease: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); + break; + case PublishResultType.FailedHasExpired: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); + break; + case PublishResultType.FailedIsTrashed: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! + break; + case PublishResultType.FailedContentInvalid: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedInvalid", + new[] + { + $"{status.Content.Name} ({status.Content.Id})", + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + case PublishResultType.FailedByCulture: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedByCulture"); // fixme properly localize! + break; + default: + throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); + } + } + + /// + /// 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 + /// + internal static bool CheckPermissions( + IDictionary storage, + IUser user, + IUserService userService, + IContentService contentService, + IEntityService entityService, + int nodeId, + char[] permissionsToCheck = null, + IContent contentItem = null) + { + 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); + } + + 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; + } + + /// + /// Used to map an instance to a and ensuring a language is present if required + /// + /// + /// + /// + private ContentItemDisplay MapToDisplay(IContent content, string culture = null) + { + //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, + // the Cuture property of ContentItemDisplay must exist (at least currently). + if (culture == null && content.ContentType.VariesByCulture()) + { + //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. + culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); + } + + var display = ContextMapper.Map(content, + new Dictionary { { ContextMapper.CultureKey, culture } }); + + return display; + } + } +} diff --git a/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs b/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs index 3e29d88d5f..6eb568c6af 100644 --- a/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/Mvc/UrlHelperExtensions.cs @@ -1,79 +1,79 @@ -using System; -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Xml; - -namespace Umbraco.Web.Mvc -{ - /// - /// Extension methods for UrlHelper - /// - public static class UrlHelperExtensions - { - /// - /// Utility method for checking for valid proxy urls or redirect urls to prevent Open Redirect security issues - /// - /// - /// The url to validate - /// The url of the current local domain (to ensure we can validate if the requested url is local without dependency on the request) - /// True if it's an allowed url - public static bool ValidateProxyUrl(this UrlHelper urlHelper, string url, string callerUrl) - { - if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) - { - return false; - } - - if (url.StartsWith("//")) - return false; - - Uri requestUri; - if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out requestUri)) - { - if (string.IsNullOrEmpty(callerUrl) == false) - { - Uri localUri; - if (Uri.TryCreate(callerUrl, UriKind.RelativeOrAbsolute, out localUri)) - { - // check for local urls - - //Cannot start with // since that is not a local url - if (requestUri.OriginalString.StartsWith("//") == false - //cannot be non-absolute and also contain the char : since that will indicate a protocol - && (requestUri.IsAbsoluteUri == false && requestUri.OriginalString.Contains(":") == false) - //needs to be non-absolute or the hosts must match the current request - && (requestUri.IsAbsoluteUri == false || requestUri.Host == localUri.Host)) - { - return true; - } - } - else - { - return false; - } - } - - //we cannot continue if the url is not absolute - if (requestUri.IsAbsoluteUri == false) - { - return false; - } - - // check for valid proxy urls - var feedProxyXml = XmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); - if (feedProxyXml != null && - feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null) - { - return true; - } - } - else - { - return false; - } - return false; - } - } -} +using System; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Xml; + +namespace Umbraco.Web.Mvc +{ + /// + /// Extension methods for UrlHelper + /// + public static class UrlHelperExtensions + { + /// + /// Utility method for checking for valid proxy urls or redirect urls to prevent Open Redirect security issues + /// + /// + /// The url to validate + /// The url of the current local domain (to ensure we can validate if the requested url is local without dependency on the request) + /// True if it's an allowed url + public static bool ValidateProxyUrl(this UrlHelper urlHelper, string url, string callerUrl) + { + if (Uri.IsWellFormedUriString(url, UriKind.RelativeOrAbsolute) == false) + { + return false; + } + + if (url.StartsWith("//")) + return false; + + Uri requestUri; + if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out requestUri)) + { + if (string.IsNullOrEmpty(callerUrl) == false) + { + Uri localUri; + if (Uri.TryCreate(callerUrl, UriKind.RelativeOrAbsolute, out localUri)) + { + // check for local urls + + //Cannot start with // since that is not a local url + if (requestUri.OriginalString.StartsWith("//") == false + //cannot be non-absolute and also contain the char : since that will indicate a protocol + && (requestUri.IsAbsoluteUri == false && requestUri.OriginalString.Contains(":") == false) + //needs to be non-absolute or the hosts must match the current request + && (requestUri.IsAbsoluteUri == false || requestUri.Host == localUri.Host)) + { + return true; + } + } + else + { + return false; + } + } + + //we cannot continue if the url is not absolute + if (requestUri.IsAbsoluteUri == false) + { + return false; + } + + // check for valid proxy urls + var feedProxyXml = XmlHelper.OpenAsXmlDocument(IOHelper.MapPath(SystemFiles.FeedProxyConfig)); + if (feedProxyXml != null && + feedProxyXml.SelectSingleNode(string.Concat("//allow[@host = '", requestUri.Host, "']")) != null) + { + return true; + } + } + else + { + return false; + } + return false; + } + } +} diff --git a/src/Umbraco.Web/UrlHelperExtensions.cs b/src/Umbraco.Web/UrlHelperExtensions.cs index 98856119c1..9b1c282aab 100644 --- a/src/Umbraco.Web/UrlHelperExtensions.cs +++ b/src/Umbraco.Web/UrlHelperExtensions.cs @@ -1,193 +1,193 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Management.Instrumentation; -using System.Web.Mvc; -using System.Web.Routing; -using ClientDependency.Core.Config; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Exceptions; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebServices; - -namespace Umbraco.Web -{ - /// - /// Extension methods for UrlHelper - /// - public static class UrlHelperExtensions - { - /// - /// Returns the base path (not including the 'action') of the MVC controller "ExamineManagementController" - /// - /// - /// - public static string GetExamineManagementServicePath(this UrlHelper url) - { - // TODO: Possibly remove this method, I think it's unused... - var result = url.GetUmbracoApiService("GetIndexerDetails"); - return result.TrimEnd("GetIndexerDetails").EnsureEndsWith('/'); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, RouteValueDictionary routeVals = null) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(actionName, typeof(T), routeVals); - } - - /// - /// Return the Base Url (not including the action) for a Web Api service - /// - /// - /// - /// - /// - public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, string actionName) - where T : UmbracoApiController - { - return url.GetUmbracoApiService(actionName).TrimEnd(actionName); - } - - public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, Expression> methodSelector) - where T : UmbracoApiController - { - var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - return url.GetUmbracoApiService(method.Name).TrimEnd(method.Name); - } - - public static string GetUmbracoApiService(this UrlHelper url, Expression> methodSelector) - where T : UmbracoApiController - { - var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); - if (method == null) - { - throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); - } - var parameters = Core.ExpressionHelper.GetMethodParams(methodSelector); - var routeVals = new RouteValueDictionary(parameters); - return url.GetUmbracoApiService(method.Name, routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, RouteValueDictionary routeVals = null) - { - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); - if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); - - var area = ""; - - var apiController = Current.UmbracoApiControllerTypes - .SingleOrDefault(x => x == apiControllerType); - if (apiController == null) - throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); - var metaData = PluginController.GetMetadata(apiController); - if (!metaData.AreaName.IsNullOrWhiteSpace()) - { - //set the area to the plugin area - area = metaData.AreaName; - } - return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) - { - return url.GetUmbracoApiService(actionName, controllerName, "", routeVals); - } - - /// - /// Return the Url for a Web Api service - /// - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, RouteValueDictionary routeVals = null) - { - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); - if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); - - if (routeVals == null) - { - routeVals = new RouteValueDictionary(new {httproute = "", area = area}); - } - else - { - var requiredRouteVals = new RouteValueDictionary(new { httproute = "", area = area }); - requiredRouteVals.MergeLeft(routeVals); - //copy it back now - routeVals = requiredRouteVals; - } - - return url.Action(actionName, controllerName, routeVals); - } - - - /// - /// Return the Url for an action with a cache-busting hash appended - /// - /// - /// - /// - /// - /// - public static string GetUrlWithCacheBust(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) - { - var applicationJs = url.Action(actionName, controllerName, routeVals); - applicationJs = applicationJs + "?umb__rnd=" + GetCacheBustHash(); - return applicationJs; - } - - /// - /// - /// - /// - public static string GetCacheBustHash() - { - //make a hash of umbraco and client dependency version - //in case the user bypasses the installer and just bumps the web.config or clientdep config - - //if in debug mode, always burst the cache - if (GlobalSettings.DebugMode) - { - return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); - } - - var version = Current.RuntimeState.SemanticVersion.ToSemanticString(); - return $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash(); - } - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Management.Instrumentation; +using System.Web.Mvc; +using System.Web.Routing; +using ClientDependency.Core.Config; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Exceptions; +using Umbraco.Web.Composing; +using Umbraco.Web.Editors; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebServices; + +namespace Umbraco.Web +{ + /// + /// Extension methods for UrlHelper + /// + public static class UrlHelperExtensions + { + /// + /// Returns the base path (not including the 'action') of the MVC controller "ExamineManagementController" + /// + /// + /// + public static string GetExamineManagementServicePath(this UrlHelper url) + { + // TODO: Possibly remove this method, I think it's unused... + var result = url.GetUmbracoApiService("GetIndexerDetails"); + return result.TrimEnd("GetIndexerDetails").EnsureEndsWith('/'); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, RouteValueDictionary routeVals = null) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(actionName, typeof(T), routeVals); + } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, string actionName) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(actionName).TrimEnd(actionName); + } + + public static string GetUmbracoApiServiceBaseUrl(this UrlHelper url, Expression> methodSelector) + where T : UmbracoApiController + { + var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + return url.GetUmbracoApiService(method.Name).TrimEnd(method.Name); + } + + public static string GetUmbracoApiService(this UrlHelper url, Expression> methodSelector) + where T : UmbracoApiController + { + var method = Core.ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + var parameters = Core.ExpressionHelper.GetMethodParams(methodSelector); + var routeVals = new RouteValueDictionary(parameters); + return url.GetUmbracoApiService(method.Name, routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, Type apiControllerType, RouteValueDictionary routeVals = null) + { + if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); + + var area = ""; + + var apiController = Current.UmbracoApiControllerTypes + .SingleOrDefault(x => x == apiControllerType); + if (apiController == null) + throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); + var metaData = PluginController.GetMetadata(apiController); + if (!metaData.AreaName.IsNullOrWhiteSpace()) + { + //set the area to the plugin area + area = metaData.AreaName; + } + return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) + { + return url.GetUmbracoApiService(actionName, controllerName, "", routeVals); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this UrlHelper url, string actionName, string controllerName, string area, RouteValueDictionary routeVals = null) + { + if (string.IsNullOrEmpty(controllerName)) throw new ArgumentNullOrEmptyException(nameof(controllerName)); + if (string.IsNullOrEmpty(actionName)) throw new ArgumentNullOrEmptyException(nameof(actionName)); + + if (routeVals == null) + { + routeVals = new RouteValueDictionary(new {httproute = "", area = area}); + } + else + { + var requiredRouteVals = new RouteValueDictionary(new { httproute = "", area = area }); + requiredRouteVals.MergeLeft(routeVals); + //copy it back now + routeVals = requiredRouteVals; + } + + return url.Action(actionName, controllerName, routeVals); + } + + + /// + /// Return the Url for an action with a cache-busting hash appended + /// + /// + /// + /// + /// + /// + public static string GetUrlWithCacheBust(this UrlHelper url, string actionName, string controllerName, RouteValueDictionary routeVals = null) + { + var applicationJs = url.Action(actionName, controllerName, routeVals); + applicationJs = applicationJs + "?umb__rnd=" + GetCacheBustHash(); + return applicationJs; + } + + /// + /// + /// + /// + public static string GetCacheBustHash() + { + //make a hash of umbraco and client dependency version + //in case the user bypasses the installer and just bumps the web.config or clientdep config + + //if in debug mode, always burst the cache + if (GlobalSettings.DebugMode) + { + return DateTime.Now.Ticks.ToString(CultureInfo.InvariantCulture).GenerateHash(); + } + + var version = Current.RuntimeState.SemanticVersion.ToSemanticString(); + return $"{version}.{ClientDependencySettings.Instance.Version}".GenerateHash(); + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs index b8f4506cb1..6d91847888 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/Tree/CustomTreeService.cs @@ -1,154 +1,154 @@ -using System; -using Umbraco.Core.Security; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Script.Services; -using System.Web.Services; -using System.Web.UI; -using umbraco; -using umbraco.cms.businesslogic; -using umbraco.cms.presentation.Trees; -using umbraco.controls.Tree; -using Umbraco.Core.Services; -using Umbraco.Web; -using Umbraco.Web.Security; -using Umbraco.Web.WebServices; - -namespace umbraco.controls.Tree -{ - /// - /// Client side ajax utlities for the tree - /// - [ScriptService] - [WebService] - public class CustomTreeService : UmbracoWebService - { - /// - /// Returns some info about the node such as path and id - /// - /// - /// - [WebMethod] - [ScriptMethod(ResponseFormat = ResponseFormat.Json)] - public NodeInfo GetNodeInfo(int id) - { - Authorize(); - - //var node = new CMSNode(id); - var node = Services.EntityService.Get(id); - return new NodeInfo() - { - Id = node.Id, - Path = node.Path, - PathAsNames = string.Join("->", - GetPathNames(node.Path.Split(',') - .Select(x => int.Parse(x)) - .ToArray())) - }; - } - - /// - /// returns the node names for each id passed in - /// - /// - /// - private string[] GetPathNames(int[] ids) - { - return ids - .Where(x => x != -1) - //.Select(x => new CMSNode(x).Text).ToArray(); - .Select(x => Services.EntityService.Get(x).Name).ToArray(); - } - - /// - /// Returns a key/value object with: json, app, js as the keys - /// - /// - [WebMethod] - [ScriptMethod(ResponseFormat = ResponseFormat.Json)] - public Dictionary GetInitAppTreeData(string app, string treeType, bool showContextMenu, bool isDialog, TreeDialogModes dialogMode, string functionToCall, string nodeKey) - { - Authorize(); - - var treeCtl = new TreeControl() - { - ShowContextMenu = showContextMenu, - IsDialog = isDialog, - DialogMode = dialogMode, - App = app, - TreeType = string.IsNullOrEmpty(treeType) ? "" : treeType, //don't set the tree type unless explicitly set - NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey, - //StartNodeID = -1, //TODO: set this based on parameters! - FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall - }; - - var returnVal = new Dictionary(); - - if (string.IsNullOrEmpty(treeType)) - { - //if there's not tree type specified, then render out the tree as per normal with the normal - //way of doing things - returnVal.Add("json", treeCtl.GetJSONInitNode()); - } - else - { - //since 4.5.1 has a bug in it, it ignores if the treeType is specified and will always only render - //the whole APP not just a specific tree. - //this is a work around for this bug until it is fixed (which should be fixed in 4.5.2 - - //get the tree that we need to render - var tree = TreeDefinitionCollection.Instance.FindTree(treeType).CreateInstance(); - tree.ShowContextMenu = showContextMenu; - tree.IsDialog = isDialog; - tree.DialogMode = dialogMode; - tree.NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey; - tree.FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall; - - //now render it's start node - var xTree = new XmlTree(); - - //we're going to hijack the node name here to make it say content/media - var node = tree.RootNode; - if (node.Text.Equals("[FilteredContentTree]")) node.Text = Services.TextService.Localize("content"); - else if (node.Text.Equals("[FilteredMediaTree]")) node.Text = Services.TextService.Localize("media"); - xTree.Add(node); - - returnVal.Add("json", xTree.ToString()); - } - - returnVal.Add("app", app); - returnVal.Add("js", treeCtl.JSCurrApp); - - return returnVal; - } - - internal void Authorize() - { - if (ValidateCurrentUser() == false) - throw new Exception("Client authorization failed. User is not logged in"); - } - - - /// - /// Validates the currently logged in user and ensures they are not timed out - /// - /// - private bool ValidateCurrentUser() - { - var identity = Context.GetCurrentIdentity( - //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! - // Without this check, anything that is using this legacy API, like ui.Text will - // automatically log the back office user in even if it is a front-end request (if there is - // a back office user logged in. This can cause problems becaues the identity is changing mid - // request. For example: http://issues.umbraco.org/issue/U4-4010 - HttpContext.Current.CurrentHandler is Page); - - if (identity != null) - { - return true; - } - return false; - } - } -} +using System; +using Umbraco.Core.Security; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using System.Web.Script.Services; +using System.Web.Services; +using System.Web.UI; +using umbraco; +using umbraco.cms.businesslogic; +using umbraco.cms.presentation.Trees; +using umbraco.controls.Tree; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Security; +using Umbraco.Web.WebServices; + +namespace umbraco.controls.Tree +{ + /// + /// Client side ajax utlities for the tree + /// + [ScriptService] + [WebService] + public class CustomTreeService : UmbracoWebService + { + /// + /// Returns some info about the node such as path and id + /// + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public NodeInfo GetNodeInfo(int id) + { + Authorize(); + + //var node = new CMSNode(id); + var node = Services.EntityService.Get(id); + return new NodeInfo() + { + Id = node.Id, + Path = node.Path, + PathAsNames = string.Join("->", + GetPathNames(node.Path.Split(',') + .Select(x => int.Parse(x)) + .ToArray())) + }; + } + + /// + /// returns the node names for each id passed in + /// + /// + /// + private string[] GetPathNames(int[] ids) + { + return ids + .Where(x => x != -1) + //.Select(x => new CMSNode(x).Text).ToArray(); + .Select(x => Services.EntityService.Get(x).Name).ToArray(); + } + + /// + /// Returns a key/value object with: json, app, js as the keys + /// + /// + [WebMethod] + [ScriptMethod(ResponseFormat = ResponseFormat.Json)] + public Dictionary GetInitAppTreeData(string app, string treeType, bool showContextMenu, bool isDialog, TreeDialogModes dialogMode, string functionToCall, string nodeKey) + { + Authorize(); + + var treeCtl = new TreeControl() + { + ShowContextMenu = showContextMenu, + IsDialog = isDialog, + DialogMode = dialogMode, + App = app, + TreeType = string.IsNullOrEmpty(treeType) ? "" : treeType, //don't set the tree type unless explicitly set + NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey, + //StartNodeID = -1, //TODO: set this based on parameters! + FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall + }; + + var returnVal = new Dictionary(); + + if (string.IsNullOrEmpty(treeType)) + { + //if there's not tree type specified, then render out the tree as per normal with the normal + //way of doing things + returnVal.Add("json", treeCtl.GetJSONInitNode()); + } + else + { + //since 4.5.1 has a bug in it, it ignores if the treeType is specified and will always only render + //the whole APP not just a specific tree. + //this is a work around for this bug until it is fixed (which should be fixed in 4.5.2 + + //get the tree that we need to render + var tree = TreeDefinitionCollection.Instance.FindTree(treeType).CreateInstance(); + tree.ShowContextMenu = showContextMenu; + tree.IsDialog = isDialog; + tree.DialogMode = dialogMode; + tree.NodeKey = string.IsNullOrEmpty(nodeKey) ? "" : nodeKey; + tree.FunctionToCall = string.IsNullOrEmpty(functionToCall) ? "" : functionToCall; + + //now render it's start node + var xTree = new XmlTree(); + + //we're going to hijack the node name here to make it say content/media + var node = tree.RootNode; + if (node.Text.Equals("[FilteredContentTree]")) node.Text = Services.TextService.Localize("content"); + else if (node.Text.Equals("[FilteredMediaTree]")) node.Text = Services.TextService.Localize("media"); + xTree.Add(node); + + returnVal.Add("json", xTree.ToString()); + } + + returnVal.Add("app", app); + returnVal.Add("js", treeCtl.JSCurrApp); + + return returnVal; + } + + internal void Authorize() + { + if (ValidateCurrentUser() == false) + throw new Exception("Client authorization failed. User is not logged in"); + } + + + /// + /// Validates the currently logged in user and ensures they are not timed out + /// + /// + private bool ValidateCurrentUser() + { + var identity = Context.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + + if (identity != null) + { + return true; + } + return false; + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs index 00788fab3f..db04f26bcf 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -1,83 +1,83 @@ -using System; -using System.Text; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.UI.Pages; -using Umbraco.Web; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; -using Umbraco.Web.WebServices; -using Umbraco.Web._Legacy.Actions; - - -namespace umbraco.dialogs -{ - public partial class AssignDomain2 : UmbracoEnsuredPage - { - protected override void OnInit(EventArgs e) - { - base.OnInit(e); - - var nodeId = GetNodeId(); - CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - - var nodeId = GetNodeId(); - var node = Services.ContentService.GetById(nodeId); - - if (node == null) - { - feedback.Text = Services.TextService.Localize("assignDomain/invalidNode"); - pane_language.Visible = false; - pane_domains.Visible = false; - p_buttons.Visible = false; - return; - } - - pane_language.Title = Services.TextService.Localize("assignDomain/setLanguage"); - pane_domains.Title = Services.TextService.Localize("assignDomain/setDomains"); - prop_language.Text = Services.TextService.Localize("assignDomain/language"); - - var nodeDomains = Services.DomainService.GetAssignedDomains(nodeId, true).ToArray(); - var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); - - var sb = new StringBuilder(); - sb.Append("languages: ["); - var i = 0; - foreach (var language in Current.Services.LocalizationService.GetAllLanguages()) - sb.AppendFormat("{0}{{ \"Id\": {1}, \"Code\": \"{2}\" }}", (i++ == 0 ? "" : ","), language.Id, language.IsoCode); - sb.Append("]\r\n"); - - sb.AppendFormat(",language: {0}", wildcard == null ? "undefined" : wildcard.LanguageId.ToString()); - - sb.Append(",domains: ["); - i = 0; - foreach (var domain in nodeDomains.Where(d => d.IsWildcard == false)) - sb.AppendFormat("{0}{{ \"Name\": \"{1}\", \"Lang\": \"{2}\" }}", (i++ == 0 ? "" :","), domain.DomainName, domain.LanguageId); - sb.Append("]\r\n"); - - data.Text = sb.ToString(); - } - - protected int GetNodeId() - { - int nodeId; - if (int.TryParse(Request.QueryString["id"], out nodeId) == false) - nodeId = -1; - return nodeId; - } - - protected string GetRestServicePath() - { - const string action = "ListDomains"; - var path = Url.GetUmbracoApiService(action); - return path.TrimEnd(action).EnsureEndsWith('/'); - } - } -} +using System; +using System.Text; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.UI.Pages; +using Umbraco.Web; +using Umbraco.Web.Composing; +using Umbraco.Web.Editors; +using Umbraco.Web.WebServices; +using Umbraco.Web._Legacy.Actions; + + +namespace umbraco.dialogs +{ + public partial class AssignDomain2 : UmbracoEnsuredPage + { + protected override void OnInit(EventArgs e) + { + base.OnInit(e); + + var nodeId = GetNodeId(); + CheckPathAndPermissions(nodeId, UmbracoObjectTypes.Document, ActionAssignDomain.Instance); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + var nodeId = GetNodeId(); + var node = Services.ContentService.GetById(nodeId); + + if (node == null) + { + feedback.Text = Services.TextService.Localize("assignDomain/invalidNode"); + pane_language.Visible = false; + pane_domains.Visible = false; + p_buttons.Visible = false; + return; + } + + pane_language.Title = Services.TextService.Localize("assignDomain/setLanguage"); + pane_domains.Title = Services.TextService.Localize("assignDomain/setDomains"); + prop_language.Text = Services.TextService.Localize("assignDomain/language"); + + var nodeDomains = Services.DomainService.GetAssignedDomains(nodeId, true).ToArray(); + var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); + + var sb = new StringBuilder(); + sb.Append("languages: ["); + var i = 0; + foreach (var language in Current.Services.LocalizationService.GetAllLanguages()) + sb.AppendFormat("{0}{{ \"Id\": {1}, \"Code\": \"{2}\" }}", (i++ == 0 ? "" : ","), language.Id, language.IsoCode); + sb.Append("]\r\n"); + + sb.AppendFormat(",language: {0}", wildcard == null ? "undefined" : wildcard.LanguageId.ToString()); + + sb.Append(",domains: ["); + i = 0; + foreach (var domain in nodeDomains.Where(d => d.IsWildcard == false)) + sb.AppendFormat("{0}{{ \"Name\": \"{1}\", \"Lang\": \"{2}\" }}", (i++ == 0 ? "" :","), domain.DomainName, domain.LanguageId); + sb.Append("]\r\n"); + + data.Text = sb.ToString(); + } + + protected int GetNodeId() + { + int nodeId; + if (int.TryParse(Request.QueryString["id"], out nodeId) == false) + nodeId = -1; + return nodeId; + } + + protected string GetRestServicePath() + { + const string action = "ListDomains"; + var path = Url.GetUmbracoApiService(action); + return path.TrimEnd(action).EnsureEndsWith('/'); + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs index 95fe3626b6..ca26c5c08d 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/nodeSorter.asmx.cs @@ -1,262 +1,262 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Web.Script.Services; -using System.Web.Services; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Web; -using Umbraco.Web.Composing; -using Umbraco.Web._Legacy.Actions; - -namespace umbraco.presentation.webservices -{ - /// - /// Summary description for nodeSorter - /// - [WebService(Namespace = "http://umbraco.org/")] - [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] - [ToolboxItem(false)] - [ScriptService] - public class nodeSorter : UmbracoAuthorizedWebService - { - [WebMethod] - public SortNode GetNodes(string ParentId, string App) - { - if (AuthorizeRequest()) - { - var nodes = new List(); - - // "hack for stylesheet" - if (App == "settings") - { - var stylesheet = Services.FileService.GetStylesheetByName(ParentId.EnsureEndsWith(".css")); - if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + ParentId); - - var sort = 0; - foreach (var child in stylesheet.Properties) - { - nodes.Add(new SortNode(child.Name.GetHashCode(), sort, child.Name, DateTime.Now)); - sort++; - } - - return new SortNode() - { - SortNodes = nodes.ToArray() - }; - } - else - { - var asInt = int.Parse(ParentId); - - var parent = new SortNode { Id = asInt }; - - var entityService = Services.EntityService; - - // Root nodes? - if (asInt == -1) - { - if (App == "media") - { - var rootMedia = entityService.GetRootEntities(UmbracoObjectTypes.Media); - nodes.AddRange(rootMedia.Select(media => new SortNode(media.Id, media.SortOrder, media.Name, media.CreateDate))); - } - else - { - var rootContent = entityService.GetRootEntities(UmbracoObjectTypes.Document); - nodes.AddRange(rootContent.Select(content => new SortNode(content.Id, content.SortOrder, content.Name, content.CreateDate))); - } - } - else - { - var children = entityService.GetChildren(asInt); - nodes.AddRange(children.Select(child => new SortNode(child.Id, child.SortOrder, child.Name, child.CreateDate))); - } - - - parent.SortNodes = nodes.ToArray(); - - return parent; - } - } - - throw new ArgumentException("User not logged in"); - } - - public void UpdateSortOrder(int ParentId, string SortOrder) - { - UpdateSortOrder(ParentId.ToString(), SortOrder); - } - - [WebMethod] - public void UpdateSortOrder(string ParentId, string SortOrder) - { - if (AuthorizeRequest() == false) return; - if (SortOrder.Trim().Length <= 0) return; - - var isContent = Context.Request.GetItemAsString("app") == "content" | Context.Request.GetItemAsString("app") == ""; - var isMedia = Context.Request.GetItemAsString("app") == "media"; - - //ensure user is authorized for the app requested - if (isContent && AuthorizeRequest(Constants.Applications.Content.ToString()) == false) return; - if (isMedia && AuthorizeRequest(Constants.Applications.Media.ToString()) == false) return; - - var ids = SortOrder.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); - if (isContent) - { - SortContent(ids, int.Parse(ParentId)); - } - else if (isMedia) - { - SortMedia(ids); - } - else - { - SortStylesheetProperties(ParentId, ids); - } - } - - private void SortMedia(string[] ids) - { - var mediaService = Services.MediaService; - var sortedMedia = new List(); - try - { - for (var i = 0; i < ids.Length; i++) - { - var id = int.Parse(ids[i]); - var m = mediaService.GetById(id); - sortedMedia.Add(m); - } - - // Save Media with new sort order and update content xml in db accordingly - var sorted = mediaService.Sort(sortedMedia); - } - catch (Exception ex) - { - Current.Logger.Error("Could not update media sort order", ex); - } - } - - - private void SortStylesheetProperties(string stylesheetName, string[] names) - { - var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); - if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); - - var currProps = stylesheet.Properties.ToArray(); - //remove them all first - foreach (var prop in currProps) - { - stylesheet.RemoveProperty(prop.Name); - } - - //re-add them in the right order - for (var i = 0; i < names.Length; i++) - { - var found = currProps.Single(x => x.Name == names[i]); - stylesheet.AddProperty(found); - } - - Services.FileService.SaveStylesheet(stylesheet); - } - - private void SortContent(string[] ids, int parentId) - { - var contentService = Services.ContentService; - try - { - // Save content with new sort order and update db+cache accordingly - var intIds = new List(); - foreach (var stringId in ids) - { - int intId; - if (int.TryParse(stringId, out intId)) - intIds.Add(intId); - } - var sorted = contentService.Sort(intIds.ToArray()); - - // refresh sort order on cached xml - // but no... this is not distributed - solely relying on content service & events should be enough - //content.Instance.SortNodes(parentId); - - //send notifications! TODO: This should be put somewhere centralized instead of hard coded directly here - if (parentId > 0) - { - Services.NotificationService.SendNotification(contentService.GetById(parentId), ActionSort.Instance, UmbracoContext, Services.TextService, GlobalSettings); - } - - } - catch (Exception ex) - { - Current.Logger.Error("Could not update content sort order", ex); - } - } - - } - - [Serializable] - public class SortNode - { - public SortNode() - { - } - - private SortNode[] _sortNodes; - - public SortNode[] SortNodes - { - get { return _sortNodes; } - set { _sortNodes = value; } - } - - public int TotalNodes - { - get { return _sortNodes != null ? _sortNodes.Length : 0; } - set { int test = value; } - } - - public SortNode(int Id, int SortOrder, string Name, DateTime CreateDate) - { - _id = Id; - _sortOrder = SortOrder; - _name = Name; - _createDate = CreateDate; - } - - private DateTime _createDate; - - public DateTime CreateDate - { - get { return _createDate; } - set { _createDate = value; } - } - - private string _name; - - public string Name - { - get { return _name; } - set { _name = value; } - } - - private int _sortOrder; - - public int SortOrder - { - get { return _sortOrder; } - set { _sortOrder = value; } - } - - private int _id; - - public int Id - { - get { return _id; } - set { _id = value; } - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Web.Script.Services; +using System.Web.Services; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Web; +using Umbraco.Web.Composing; +using Umbraco.Web._Legacy.Actions; + +namespace umbraco.presentation.webservices +{ + /// + /// Summary description for nodeSorter + /// + [WebService(Namespace = "http://umbraco.org/")] + [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] + [ToolboxItem(false)] + [ScriptService] + public class nodeSorter : UmbracoAuthorizedWebService + { + [WebMethod] + public SortNode GetNodes(string ParentId, string App) + { + if (AuthorizeRequest()) + { + var nodes = new List(); + + // "hack for stylesheet" + if (App == "settings") + { + var stylesheet = Services.FileService.GetStylesheetByName(ParentId.EnsureEndsWith(".css")); + if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + ParentId); + + var sort = 0; + foreach (var child in stylesheet.Properties) + { + nodes.Add(new SortNode(child.Name.GetHashCode(), sort, child.Name, DateTime.Now)); + sort++; + } + + return new SortNode() + { + SortNodes = nodes.ToArray() + }; + } + else + { + var asInt = int.Parse(ParentId); + + var parent = new SortNode { Id = asInt }; + + var entityService = Services.EntityService; + + // Root nodes? + if (asInt == -1) + { + if (App == "media") + { + var rootMedia = entityService.GetRootEntities(UmbracoObjectTypes.Media); + nodes.AddRange(rootMedia.Select(media => new SortNode(media.Id, media.SortOrder, media.Name, media.CreateDate))); + } + else + { + var rootContent = entityService.GetRootEntities(UmbracoObjectTypes.Document); + nodes.AddRange(rootContent.Select(content => new SortNode(content.Id, content.SortOrder, content.Name, content.CreateDate))); + } + } + else + { + var children = entityService.GetChildren(asInt); + nodes.AddRange(children.Select(child => new SortNode(child.Id, child.SortOrder, child.Name, child.CreateDate))); + } + + + parent.SortNodes = nodes.ToArray(); + + return parent; + } + } + + throw new ArgumentException("User not logged in"); + } + + public void UpdateSortOrder(int ParentId, string SortOrder) + { + UpdateSortOrder(ParentId.ToString(), SortOrder); + } + + [WebMethod] + public void UpdateSortOrder(string ParentId, string SortOrder) + { + if (AuthorizeRequest() == false) return; + if (SortOrder.Trim().Length <= 0) return; + + var isContent = Context.Request.GetItemAsString("app") == "content" | Context.Request.GetItemAsString("app") == ""; + var isMedia = Context.Request.GetItemAsString("app") == "media"; + + //ensure user is authorized for the app requested + if (isContent && AuthorizeRequest(Constants.Applications.Content.ToString()) == false) return; + if (isMedia && AuthorizeRequest(Constants.Applications.Media.ToString()) == false) return; + + var ids = SortOrder.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + if (isContent) + { + SortContent(ids, int.Parse(ParentId)); + } + else if (isMedia) + { + SortMedia(ids); + } + else + { + SortStylesheetProperties(ParentId, ids); + } + } + + private void SortMedia(string[] ids) + { + var mediaService = Services.MediaService; + var sortedMedia = new List(); + try + { + for (var i = 0; i < ids.Length; i++) + { + var id = int.Parse(ids[i]); + var m = mediaService.GetById(id); + sortedMedia.Add(m); + } + + // Save Media with new sort order and update content xml in db accordingly + var sorted = mediaService.Sort(sortedMedia); + } + catch (Exception ex) + { + Current.Logger.Error("Could not update media sort order", ex); + } + } + + + private void SortStylesheetProperties(string stylesheetName, string[] names) + { + var stylesheet = Services.FileService.GetStylesheetByName(stylesheetName.EnsureEndsWith(".css")); + if (stylesheet == null) throw new InvalidOperationException("No stylesheet found by name " + stylesheetName); + + var currProps = stylesheet.Properties.ToArray(); + //remove them all first + foreach (var prop in currProps) + { + stylesheet.RemoveProperty(prop.Name); + } + + //re-add them in the right order + for (var i = 0; i < names.Length; i++) + { + var found = currProps.Single(x => x.Name == names[i]); + stylesheet.AddProperty(found); + } + + Services.FileService.SaveStylesheet(stylesheet); + } + + private void SortContent(string[] ids, int parentId) + { + var contentService = Services.ContentService; + try + { + // Save content with new sort order and update db+cache accordingly + var intIds = new List(); + foreach (var stringId in ids) + { + int intId; + if (int.TryParse(stringId, out intId)) + intIds.Add(intId); + } + var sorted = contentService.Sort(intIds.ToArray()); + + // refresh sort order on cached xml + // but no... this is not distributed - solely relying on content service & events should be enough + //content.Instance.SortNodes(parentId); + + //send notifications! TODO: This should be put somewhere centralized instead of hard coded directly here + if (parentId > 0) + { + Services.NotificationService.SendNotification(contentService.GetById(parentId), ActionSort.Instance, UmbracoContext, Services.TextService, GlobalSettings); + } + + } + catch (Exception ex) + { + Current.Logger.Error("Could not update content sort order", ex); + } + } + + } + + [Serializable] + public class SortNode + { + public SortNode() + { + } + + private SortNode[] _sortNodes; + + public SortNode[] SortNodes + { + get { return _sortNodes; } + set { _sortNodes = value; } + } + + public int TotalNodes + { + get { return _sortNodes != null ? _sortNodes.Length : 0; } + set { int test = value; } + } + + public SortNode(int Id, int SortOrder, string Name, DateTime CreateDate) + { + _id = Id; + _sortOrder = SortOrder; + _name = Name; + _createDate = CreateDate; + } + + private DateTime _createDate; + + public DateTime CreateDate + { + get { return _createDate; } + set { _createDate = value; } + } + + private string _name; + + public string Name + { + get { return _name; } + set { _name = value; } + } + + private int _sortOrder; + + public int SortOrder + { + get { return _sortOrder; } + set { _sortOrder = value; } + } + + private int _id; + + public int Id + { + get { return _id; } + set { _id = value; } + } + } +}