diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs index 45c6313d91..dfd2cd2344 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentDomainsAndCulture.cs @@ -10,6 +10,6 @@ namespace Umbraco.Web.Models.ContentEditing public IEnumerable Domains { get; set; } [DataMember(Name = "language")] - public string Language { get; internal set; } + public string Language { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs index 195c1800b7..001f8ed499 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemSave.cs @@ -55,7 +55,7 @@ namespace Umbraco.Web.Models.ContentEditing //Non explicit internal getter so we don't need to explicitly cast in our own code [IgnoreDataMember] - internal IContent PersistedContent + public IContent PersistedContent { get => ((IContentSave)this).PersistedContent; set => ((IContentSave)this).PersistedContent = value; diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs index 2b70a63035..73845f8461 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyBasic.cs @@ -68,6 +68,6 @@ namespace Umbraco.Web.Models.ContentEditing /// Used internally during model mapping /// [IgnoreDataMember] - internal IDataEditor PropertyEditor { get; set; } + public IDataEditor PropertyEditor { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs index 9a7555ad92..45c30bcd25 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentVariantSave.cs @@ -69,6 +69,6 @@ namespace Umbraco.Web.Models.ContentEditing /// This is not used for outgoing model information. /// [IgnoreDataMember] - internal ContentPropertyCollectionDto PropertyCollectionDto { get; set; } + public ContentPropertyCollectionDto PropertyCollectionDto { get; set; } } } diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index e6566cddf1..85347abb42 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -288,7 +288,7 @@ namespace Umbraco.Web.Routing /// The domain name to parse /// The currently requested URI. If the domain name is relative, the authority of URI will be used. /// The domain name as a URI - internal static Uri ParseUriFromDomainName(string domainName, Uri currentUri) + public static Uri ParseUriFromDomainName(string domainName, Uri currentUri) { // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri var name = domainName.StartsWith("/") && currentUri != null diff --git a/src/Umbraco.Tests.Integration/TestServer/Controllers/BackOfficeAssetsControllerTests.cs b/src/Umbraco.Tests.Integration/TestServer/Controllers/BackOfficeAssetsControllerTests.cs new file mode 100644 index 0000000000..53feaa7cc0 --- /dev/null +++ b/src/Umbraco.Tests.Integration/TestServer/Controllers/BackOfficeAssetsControllerTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Controllers; + +namespace Umbraco.Tests.Integration.TestServer.Controllers +{ + public class BackOfficeAssetsControllerTests: UmbracoWebApplicationFactory + { + private LinkGenerator _linkGenerator; + + [OneTimeSetUp] + public void GivenARequestToTheController() + { + _linkGenerator = Services.GetRequiredService(); + } + + [Test] + public async Task EnsureSuccessStatusCode() + { + // Arrange + var client = CreateClient(); + var url = _linkGenerator.GetUmbracoApiService(x=>x.GetSupportedLocales()); + + // Act + var response = await client.GetAsync(url); + + // Assert + Assert.GreaterOrEqual((int)response.StatusCode, 200); + Assert.Less((int)response.StatusCode, 300); + } + } +} diff --git a/src/Umbraco.Tests.Integration/TestServer/UmbracoWebApplicationFactory.cs b/src/Umbraco.Tests.Integration/TestServer/UmbracoWebApplicationFactory.cs new file mode 100644 index 0000000000..9be8c7099b --- /dev/null +++ b/src/Umbraco.Tests.Integration/TestServer/UmbracoWebApplicationFactory.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Testing; +using Umbraco.Web.UI.BackOffice; + +namespace Umbraco.Tests.Integration.TestServer +{ + public class UmbracoWebApplicationFactory : WebApplicationFactory + { + } +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 1b49f4d7bc..cc8e1f5bb5 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/src/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -15,6 +15,7 @@ + @@ -30,6 +31,7 @@ + diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 9f7134dd81..548afbdd88 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -172,10 +172,10 @@ namespace Umbraco.Web.BackOffice.Controllers // "userGroupsApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( // controller => controller.PostSaveUserGroup(null)) // }, - // { - // "contentApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - // controller => controller.PostSave(null)) - // }, + { + "contentApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.PostSave(null)) + }, // { // "mediaApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( // controller => controller.GetRootMedia()) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs similarity index 78% rename from src/Umbraco.Web/Editors/ContentController.cs rename to src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 0bb9f93b83..22b062d5a8 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -5,15 +5,13 @@ 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 Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Entities; @@ -26,20 +24,18 @@ using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Actions; -using Umbraco.Web.Composing; using Umbraco.Web.ContentApps; -using Umbraco.Web.Editors.Binders; -using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; -using Umbraco.Web.Mvc; using Umbraco.Web.Routing; -using Umbraco.Core.Collections; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; -using Umbraco.Core.Strings; -using Umbraco.Core.Mapping; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Models.Mapping; +using Umbraco.Web.Security; +using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Editors { @@ -52,45 +48,76 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [ContentControllerConfiguration] public class ContentController : ContentControllerBase { private readonly PropertyEditorCollection _propertyEditors; + private readonly IContentService _contentService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IUserService _userService; + private readonly IWebSecurity _webSecurity; + private readonly IEntityService _entityService; + private readonly IContentTypeService _contentTypeService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IPublicAccessService _publicAccessService; + private readonly IDomainService _domainService; + private readonly IDataTypeService _dataTypeService; + private readonly ILocalizationService _localizationService; + private readonly IMemberService _memberService; + private readonly IFileService _fileService; + private readonly INotificationService _notificationService; + private readonly ActionCollection _actionCollection; + private readonly IMemberGroupService _memberGroupService; + private readonly ISqlContext _sqlContext; private readonly Lazy> _allLangs; public object Domains { get; private set; } public ContentController( ICultureDictionary cultureDictionary, - PropertyEditorCollection propertyEditors, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, + ILogger logger, IShortStringHelper shortStringHelper, + EventMessages eventMessages, + ILocalizedTextService localizedTextService, + PropertyEditorCollection propertyEditors, + IContentService contentService, + IUserService userService, + IWebSecurity webSecurity, + IEntityService entityService, + IContentTypeService contentTypeService, UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - : base(cultureDictionary, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IPublishedUrlProvider publishedUrlProvider, + IPublicAccessService publicAccessService, + IDomainService domainService, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IMemberService memberService, + IFileService fileService, + INotificationService notificationService, + ActionCollection actionCollection, + IMemberGroupService memberGroupService, + ISqlContext sqlContext) + : base(cultureDictionary, logger, shortStringHelper, eventMessages, localizedTextService) { - _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); - _allLangs = new Lazy>(() => Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.IsoCode, x => x, StringComparer.InvariantCultureIgnoreCase)); - } - - /// - /// 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)) - )); - } + _propertyEditors = propertyEditors; + _contentService = contentService; + _localizedTextService = localizedTextService; + _userService = userService; + _webSecurity = webSecurity; + _entityService = entityService; + _contentTypeService = contentTypeService; + _umbracoMapper = umbracoMapper; + _publishedUrlProvider = publishedUrlProvider; + _publicAccessService = publicAccessService; + _domainService = domainService; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _memberService = memberService; + _fileService = fileService; + _notificationService = notificationService; + _actionCollection = actionCollection; + _memberGroupService = memberGroupService; + _sqlContext = sqlContext; } /// @@ -98,10 +125,10 @@ namespace Umbraco.Web.Editors /// /// [HttpGet] - [WebApi.UmbracoAuthorize, OverrideAuthorization] + [UmbracoAuthorize] public bool AllowsCultureVariation() { - var contentTypes = Services.ContentTypeService.GetAll(); + var contentTypes = _contentTypeService.GetAll(); return contentTypes.Any(contentType => contentType.VariesByCulture()); } @@ -111,9 +138,9 @@ namespace Umbraco.Web.Editors /// /// [FilterAllowedOutgoingContent(typeof(IEnumerable))] - public IEnumerable GetByIds([FromUri]int[] ids) + public IEnumerable GetByIds([FromQuery]int[] ids) { - var foundContent = Services.ContentService.GetByIds(ids); + var foundContent = _contentService.GetByIds(ids); return foundContent.Select(MapToDisplay); } @@ -126,20 +153,20 @@ namespace Umbraco.Web.Editors /// Permission check is done for letter 'R' which is for which the user must have access to update /// [EnsureUserPermissionForContent("saveModel.ContentId", 'R')] - public IEnumerable PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) + public ActionResult> PostSaveUserGroupPermissions(UserGroupPermissionsSave saveModel) { - if (saveModel.ContentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + if (saveModel.ContentId <= 0) return NotFound(); // TODO: Should non-admins be allowed to set granular permissions? - var content = Services.ContentService.GetById(saveModel.ContentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = _contentService.GetById(saveModel.ContentId); + if (content == null) return NotFound(); //current permissions explicitly assigned to this content item - var contentPermissions = Services.ContentService.GetPermissions(content) + var contentPermissions = _contentService.GetPermissions(content) .ToDictionary(x => x.UserGroupId, x => x); - var allUserGroups = Services.UserService.GetAllUserGroups().ToArray(); + var allUserGroups = _userService.GetAllUserGroups().ToArray(); //loop through each user group foreach (var userGroup in allUserGroups) @@ -155,7 +182,7 @@ namespace Umbraco.Web.Editors //for this group/node which will go back to the defaults if (groupPermissionCodes.Length == 0) { - Services.UserService.RemoveUserGroupPermissions(userGroup.Id, content.Id); + _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)) @@ -164,14 +191,14 @@ namespace Umbraco.Web.Editors 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); + _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); + _userService.ReplaceUserGroupPermissions(userGroup.Id, groupPermissionCodes.Select(x => x[0]), content.Id); } } } @@ -188,31 +215,31 @@ namespace Umbraco.Web.Editors /// Permission check is done for letter 'R' which is for which the user must have access to view /// [EnsureUserPermissionForContent("contentId", 'R')] - public IEnumerable GetDetailedPermissions(int contentId) + public ActionResult> 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)); + if (contentId <= 0) return NotFound(); + var content = _contentService.GetById(contentId); + if (content == null) return NotFound(); // TODO: Should non-admins be able to see detailed permissions? - var allUserGroups = Services.UserService.GetAllUserGroups(); + var allUserGroups = _userService.GetAllUserGroups(); return GetDetailedPermissions(content, allUserGroups); } - private IEnumerable GetDetailedPermissions(IContent content, IEnumerable allUserGroups) + private ActionResult> 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.MapEnumerable(allUserGroups); + var defaultPermissionsByGroup = _umbracoMapper.MapEnumerable(allUserGroups); var defaultPermissionsAsDictionary = defaultPermissionsByGroup .ToDictionary(x => Convert.ToInt32(x.Id), x => x); //get the actual assigned permissions - var assignedPermissionsByGroup = Services.ContentService.GetPermissions(content).ToArray(); + var assignedPermissionsByGroup = _contentService.GetPermissions(content).ToArray(); //iterate over assigned and update the defaults with the real values foreach (var assignedGroupPermission in assignedPermissionsByGroup) @@ -239,10 +266,10 @@ namespace Umbraco.Web.Editors /// Returns an item to be used to display the recycle bin for content /// /// - public ContentItemDisplay GetRecycleBin() + public ActionResult GetRecycleBin() { var apps = new List(); - apps.Add(ListViewContentAppFactory.CreateContentApp(Services.DataTypeService, _propertyEditors, "recycleBin", "content", Core.Constants.DataTypes.DefaultMembersListView)); + apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "content", Core.Constants.DataTypes.DefaultMembersListView)); apps[0].Active = true; var display = new ContentItemDisplay { @@ -256,7 +283,7 @@ namespace Umbraco.Web.Editors new ContentVariantDisplay { CreateDate = DateTime.Now, - Name = Services.TextService.Localize("general/recycleBin") + Name = _localizedTextService.Localize("general/recycleBin") } }, ContentApps = apps @@ -265,12 +292,12 @@ namespace Umbraco.Web.Editors return display; } - public ContentItemDisplay GetBlueprintById(int id) + public ActionResult GetBlueprintById(int id) { - var foundContent = Services.ContentService.GetBlueprintById(id); + var foundContent = _contentService.GetBlueprintById(id); if (foundContent == null) { - HandleContentNotFound(id); + return HandleContentNotFound(id); } var content = MapToDisplay(foundContent); @@ -303,11 +330,12 @@ namespace Umbraco.Web.Editors /// /// /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] [EnsureUserPermissionForContent("id")] + [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(int id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { HandleContentNotFound(id); @@ -322,11 +350,12 @@ namespace Umbraco.Web.Editors /// /// /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] [EnsureUserPermissionForContent("id")] + [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Guid id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { HandleContentNotFound(id); @@ -342,8 +371,9 @@ namespace Umbraco.Web.Editors /// /// /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] [EnsureUserPermissionForContent("id")] + [DetermineAmbiguousActionByPassingParameters] public ContentItemDisplay GetById(Udi id) { var guidUdi = id as GuidUdi; @@ -360,22 +390,22 @@ namespace Umbraco.Web.Editors /// /// /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) { - var contentType = Services.ContentTypeService.Get(contentTypeAlias); + var contentType = _contentTypeService.Get(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); + var emptyContent = _contentService.Create("", parentId, contentType.Alias, _webSecurity.GetUserId().ResultOr(0)); var mapped = MapToDisplay(emptyContent); // translate the content type name if applicable - mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.ContentTypeName); + mapped.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.ContentTypeName); // if your user type doesn't have access to the Settings section it would not get this property mapped if (mapped.DocumentType != null) - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.DocumentType.Name); + mapped.DocumentType.Name = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, mapped.DocumentType.Name); //remove the listview app if it exists mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); @@ -383,10 +413,10 @@ namespace Umbraco.Web.Editors return mapped; } - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public ContentItemDisplay GetEmpty(int blueprintId, int parentId) { - var blueprint = Services.ContentService.GetBlueprintById(blueprintId); + var blueprint = _contentService.GetBlueprintById(blueprintId); if (blueprint == null) { throw new HttpResponseException(HttpStatusCode.NotFound); @@ -396,7 +426,7 @@ namespace Umbraco.Web.Editors blueprint.Name = string.Empty; blueprint.ParentId = parentId; - var mapped = Mapper.Map(blueprint); + var mapped = _umbracoMapper.Map(blueprint); //remove the listview app if it exists mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); @@ -409,12 +439,11 @@ namespace Umbraco.Web.Editors /// /// /// - public HttpResponseMessage GetNiceUrl(int id) + [DetermineAmbiguousActionByPassingParameters] + public IActionResult GetNiceUrl(int id) { - var url = PublishedUrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "text/plain"); - return response; + var url = _publishedUrlProvider.GetUrl(id); + return Content(url, "text/plain", Encoding.UTF8); } /// @@ -422,12 +451,11 @@ namespace Umbraco.Web.Editors /// /// /// - public HttpResponseMessage GetNiceUrl(Guid id) + [DetermineAmbiguousActionByPassingParameters] + public IActionResult GetNiceUrl(Guid id) { - var url = PublishedUrlProvider.GetUrl(id); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(url, Encoding.UTF8, "text/plain"); - return response; + var url = _publishedUrlProvider.GetUrl(id); + return Content(url, "text/plain", Encoding.UTF8); } /// @@ -435,14 +463,17 @@ namespace Umbraco.Web.Editors /// /// /// - public HttpResponseMessage GetNiceUrl(Udi id) + [DetermineAmbiguousActionByPassingParameters] + public IActionResult GetNiceUrl(Udi id) { var guidUdi = id as GuidUdi; if (guidUdi != null) { return GetNiceUrl(guidUdi.Guid); + } - throw new HttpResponseException(HttpStatusCode.NotFound); + + return NotFound(); } /// @@ -496,11 +527,11 @@ namespace Umbraco.Web.Editors if (filter.IsNullOrWhiteSpace() == false) { //add the default text filter - queryFilter = SqlContext.Query() + queryFilter = _sqlContext.Query() .Where(x => x.Name.Contains(filter)); } - children = Services.ContentService + children = _contentService .GetPagedChildren(id, pageNumber - 1, pageSize, out totalChildren, queryFilter, Ordering.By(orderBy, orderDirection, cultureName, !orderBySystemField)).ToList(); @@ -508,7 +539,7 @@ namespace Umbraco.Web.Editors else { //better to not use this without paging where possible, currently only the sort dialog does - children = Services.ContentService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList(); + children = _contentService.GetPagedChildren(id, 0, int.MaxValue, out var total).ToList(); totalChildren = children.Count; } @@ -519,7 +550,7 @@ namespace Umbraco.Web.Editors var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); pagedResult.Items = children.Select(content => - Mapper.Map>(content, + _umbracoMapper.Map>(content, context => { @@ -541,25 +572,25 @@ namespace Umbraco.Web.Editors /// The name of the blueprint /// [HttpPost] - public SimpleNotificationModel CreateBlueprintFromContent([FromUri]int contentId, [FromUri]string name) + public ActionResult CreateBlueprintFromContent([FromQuery]int contentId, [FromQuery]string name) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); - var content = Services.ContentService.GetById(contentId); + var content = _contentService.GetById(contentId); if (content == null) - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + return NotFound(); EnsureUniqueName(name, content, nameof(name)); - var blueprint = Services.ContentService.CreateContentFromBlueprint(content, name, Security.GetUserId().ResultOr(0)); + var blueprint = _contentService.CreateContentFromBlueprint(content, name, _webSecurity.GetUserId().ResultOr(0)); - Services.ContentService.SaveBlueprint(blueprint, Security.GetUserId().ResultOr(0)); + _contentService.SaveBlueprint(blueprint, _webSecurity.GetUserId().ResultOr(0)); var notificationModel = new SimpleNotificationModel(); notificationModel.AddSuccessNotification( - Services.TextService.Localize("blueprints/createdBlueprintHeading"), - Services.TextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) + _localizedTextService.Localize("blueprints/createdBlueprintHeading"), + _localizedTextService.Localize("blueprints/createdBlueprintMessage", new[] { content.Name }) ); return notificationModel; @@ -567,11 +598,11 @@ namespace Umbraco.Web.Editors private void EnsureUniqueName(string name, IContent content, string modelName) { - var existing = Services.ContentService.GetBlueprintsForContentTypes(content.ContentTypeId); + var existing = _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)); + ModelState.AddModelError(modelName, _localizedTextService.Localize("blueprints/duplicateBlueprintMessage")); + throw HttpResponseException.CreateValidationErrorResponse(ModelState); } } @@ -588,7 +619,7 @@ namespace Umbraco.Web.Editors { EnsureUniqueName(content.Name, content, "Name"); - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + _contentService.SaveBlueprint(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); //we need to reuse the underlying logic so return the result that it wants return OperationResult.Succeed(new EventMessages()); }, @@ -608,12 +639,12 @@ namespace Umbraco.Web.Editors /// [FileUploadCleanupFilter] [ContentSaveValidation] - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public ContentItemDisplay PostSave([ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { var contentItemDisplay = PostSaveInternal( contentItem, - content => Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id), + content => _contentService.Save(contentItem.PersistedContent, _webSecurity.CurrentUser.Id), MapToDisplay); return contentItemDisplay; @@ -652,7 +683,7 @@ namespace Umbraco.Web.Editors // add the model state to the outgoing object and throw a validation message var forDisplay = mapToDisplay(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + throw HttpResponseException.CreateValidationErrorResponse(forDisplay); } //if there's only one variant and the model state is not valid we cannot publish so change it to save @@ -715,7 +746,7 @@ namespace Umbraco.Web.Editors case ContentSaveAction.SendPublish: case ContentSaveAction.SendPublishNew: - var sendResult = Services.ContentService.SendToPublication(contentItem.PersistedContent, Security.CurrentUser.Id); + var sendResult = _contentService.SendToPublication(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); wasCancelled = sendResult == false; if (sendResult) { @@ -732,15 +763,15 @@ namespace Umbraco.Web.Editors var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName })); + _localizedTextService.Localize("speechBubbles/editContentSendToPublish"), + _localizedTextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editContentSendToPublishText")); + _localizedTextService.Localize("speechBubbles/editContentSendToPublish"), + _localizedTextService.Localize("speechBubbles/editContentSendToPublishText")); } } break; @@ -757,8 +788,8 @@ namespace Umbraco.Web.Editors if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/invalidPublishBranchPermissions")); + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -773,8 +804,8 @@ namespace Umbraco.Web.Editors if (!ValidatePublishBranchPermissions(contentItem, out var noAccess)) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/invalidPublishBranchPermissions")); + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/invalidPublishBranchPermissions")); wasCancelled = false; break; } @@ -809,7 +840,7 @@ namespace Umbraco.Web.Editors //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! - throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); + throw HttpResponseException.CreateValidationErrorResponse(display); } } @@ -929,15 +960,15 @@ namespace Umbraco.Web.Editors var variantName = GetVariantName(culture, segment); AddSuccessNotification(notifications, culture, segment, - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize(variantSavedLocalizationKey, new[] { variantName })); + _localizedTextService.Localize("speechBubbles/editContentSavedHeader"), + _localizedTextService.Localize(variantSavedLocalizationKey, new[] { variantName })); } } else if (ModelState.IsValid) { globalNotifications.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize(invariantSavedLocalizationKey)); + _localizedTextService.Localize("speechBubbles/editContentSavedHeader"), + _localizedTextService.Localize(invariantSavedLocalizationKey)); } } } @@ -968,8 +999,8 @@ namespace Umbraco.Web.Editors if (variant.ReleaseDate.HasValue && variant.ReleaseDate < DateTime.Now) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("speechBubbles", "validationFailedHeader"), - Services.TextService.Localize("speechBubbles", "scheduleErrReleaseDate1")); + _localizedTextService.Localize("speechBubbles", "validationFailedHeader"), + _localizedTextService.Localize("speechBubbles", "scheduleErrReleaseDate1")); return false; } @@ -977,8 +1008,8 @@ namespace Umbraco.Web.Editors if (variant.ExpireDate.HasValue && variant.ExpireDate < DateTime.Now) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("speechBubbles", "validationFailedHeader"), - Services.TextService.Localize("speechBubbles", "scheduleErrExpireDate1")); + _localizedTextService.Localize("speechBubbles", "validationFailedHeader"), + _localizedTextService.Localize("speechBubbles", "scheduleErrExpireDate1")); return false; } @@ -986,8 +1017,8 @@ namespace Umbraco.Web.Editors if (variant.ExpireDate.HasValue && variant.ReleaseDate.HasValue && variant.ExpireDate <= variant.ReleaseDate) { globalNotifications.AddErrorNotification( - Services.TextService.Localize("speechBubbles", "validationFailedHeader"), - Services.TextService.Localize("speechBubbles", "scheduleErrExpireDate2")); + _localizedTextService.Localize("speechBubbles", "validationFailedHeader"), + _localizedTextService.Localize("speechBubbles", "scheduleErrExpireDate2")); return false; } @@ -1149,7 +1180,7 @@ namespace Umbraco.Web.Editors var total = long.MaxValue; while (page * pageSize < total) { - var descendants = Services.EntityService.GetPagedDescendants(contentItem.Id, UmbracoObjectTypes.Document, page++, pageSize, out total, + var descendants = _entityService.GetPagedDescendants(contentItem.Id, UmbracoObjectTypes.Document, page++, pageSize, out total, //order by shallowest to deepest, this allows us to check permissions from top to bottom so we can exit //early if a permission higher up fails ordering: Ordering.By("path", Direction.Ascending)); @@ -1159,7 +1190,7 @@ namespace Umbraco.Web.Editors //if this item's path has already been denied or if the user doesn't have access to it, add to the deny list if (denied.Any(x => c.Path.StartsWith($"{x.Path},")) || (ContentPermissionsHelper.CheckPermissions(c, - Security.CurrentUser, Services.UserService, Services.EntityService, + _webSecurity.CurrentUser, _userService, _entityService, ActionPublish.ActionLetter) == ContentPermissionsHelper.ContentAccess.Denied)) { denied.Add(c); @@ -1176,7 +1207,7 @@ namespace Umbraco.Web.Editors if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally - var publishStatus = Services.ContentService.SaveAndPublishBranch(contentItem.PersistedContent, force, userId: Security.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent, force, userId: _webSecurity.CurrentUser.Id); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = null; //must be null! this implies invariant @@ -1211,7 +1242,7 @@ namespace Umbraco.Web.Editors if (canPublish) { //proceed to publish if all validation still succeeds - var publishStatus = Services.ContentService.SaveAndPublishBranch(contentItem.PersistedContent, force, culturesToPublish, Security.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublishBranch(contentItem.PersistedContent, force, culturesToPublish, _webSecurity.CurrentUser.Id); // TODO: Deal with multiple cancellations wasCancelled = publishStatus.Any(x => x.Result == PublishResultType.FailedPublishCancelledByEvent); successfulCultures = contentItem.Variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); @@ -1220,7 +1251,7 @@ namespace Umbraco.Web.Editors else { //can only save - var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); + var saveResult = _contentService.Save(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); var publishStatus = new[] { new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent) @@ -1248,7 +1279,7 @@ namespace Umbraco.Web.Editors if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally - var publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent, userId: _webSecurity.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = null; //must be null! this implies invariant return publishStatus; @@ -1293,7 +1324,7 @@ namespace Umbraco.Web.Editors if (canPublish) { //proceed to publish if all validation still succeeds - var publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, culturesToPublish, Security.CurrentUser.Id); + var publishStatus = _contentService.SaveAndPublish(contentItem.PersistedContent, culturesToPublish, _webSecurity.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedPublishCancelledByEvent; successfulCultures = culturesToPublish; return publishStatus; @@ -1301,7 +1332,7 @@ namespace Umbraco.Web.Editors else { //can only save - var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); + var saveResult = _contentService.Save(contentItem.PersistedContent, _webSecurity.CurrentUser.Id); var publishStatus = new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, null, contentItem.PersistedContent); wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; successfulCultures = Array.Empty(); @@ -1410,7 +1441,7 @@ namespace Umbraco.Web.Editors var cultureToUse = cultureToken ?? culture; var variantName = GetVariantName(cultureToUse, segment); - var errMsg = Services.TextService.Localize(localizationKey, new[] { variantName }); + var errMsg = _localizedTextService.Localize(localizationKey, new[] { variantName }); ModelState.AddVariantValidationError(culture, segment, errMsg); } @@ -1447,42 +1478,41 @@ namespace Umbraco.Web.Editors /// /// [EnsureUserPermissionForContent("id", 'U')] - public HttpResponseMessage PostPublishById(int id) + public IActionResult PostPublishById(int id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { return HandleContentNotFound(id, false); } - var publishResult = Services.ContentService.SaveAndPublish(foundContent, userId: Security.GetUserId().ResultOr(0)); + var publishResult = _contentService.SaveAndPublish(foundContent, userId: _webSecurity.GetUserId().ResultOr(0)); if (publishResult.Success == false) { var notificationModel = new SimpleNotificationModel(); AddMessageForPublishStatus(new[] { publishResult }, notificationModel); - return Request.CreateValidationErrorResponse(notificationModel); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } - //return ok - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } [HttpDelete] [HttpPost] - public HttpResponseMessage DeleteBlueprint(int id) + public IActionResult DeleteBlueprint(int id) { - var found = Services.ContentService.GetBlueprintById(id); + var found = _contentService.GetBlueprintById(id); if (found == null) { return HandleContentNotFound(id, false); } - Services.ContentService.DeleteBlueprint(found); + _contentService.DeleteBlueprint(found); - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } /// @@ -1497,9 +1527,9 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent("id", ActionDelete.ActionLetter)] [HttpDelete] [HttpPost] - public HttpResponseMessage DeleteById(int id) + public IActionResult DeleteById(int id) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); if (foundContent == null) { @@ -1509,26 +1539,26 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundContent.Trashed == false) { - var moveResult = Services.ContentService.MoveToRecycleBin(foundContent, Security.GetUserId().ResultOr(0)); + var moveResult = _contentService.MoveToRecycleBin(foundContent, _webSecurity.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()); + throw HttpResponseException.CreateValidationErrorResponse(new SimpleNotificationModel()); } } else { - var deleteResult = Services.ContentService.Delete(foundContent, Security.GetUserId().ResultOr(0)); + var deleteResult = _contentService.Delete(foundContent, _webSecurity.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()); + throw HttpResponseException.CreateValidationErrorResponse(new SimpleNotificationModel()); } } - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } /// @@ -1543,9 +1573,9 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent(Constants.System.RecycleBinContent, ActionDelete.ActionLetter)] public HttpResponseMessage EmptyRecycleBin() { - Services.ContentService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + _contentService.EmptyRecycleBin(_webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Request.CreateNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); } /// @@ -1554,33 +1584,33 @@ namespace Umbraco.Web.Editors /// /// [EnsureUserPermissionForContent("sorted.ParentId", 'S')] - public HttpResponseMessage PostSort(ContentSortOrder sorted) + public IActionResult PostSort(ContentSortOrder sorted) { if (sorted == null) { - return Request.CreateResponse(HttpStatusCode.NotFound); + return NotFound(); } //if there's nothing to sort just return ok if (sorted.IdSortOrder.Length == 0) { - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } try { - var contentService = Services.ContentService; + var contentService = _contentService; // Save content with new sort order and update content xml in db accordingly - var sortResult = contentService.Sort(sorted.IdSortOrder, Security.CurrentUser.Id); + var sortResult = contentService.Sort(sorted.IdSortOrder, _webSecurity.CurrentUser.Id); if (!sortResult.Success) { Logger.Warn("Content sorting failed, this was probably caused by an event being cancelled"); // TODO: Now you can cancel sorting, does the event messages bubble up automatically? - return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); + throw HttpResponseException.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); } - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } catch (Exception ex) { @@ -1595,15 +1625,13 @@ namespace Umbraco.Web.Editors /// /// [EnsureUserPermissionForContent("move.ParentId", 'M')] - public HttpResponseMessage PostMove(MoveOrCopy move) + public IActionResult PostMove(MoveOrCopy move) { var toMove = ValidateMoveOrCopy(move); - Services.ContentService.Move(toMove, move.ParentId, Security.GetUserId().ResultOr(0)); + _contentService.Move(toMove, move.ParentId, _webSecurity.GetUserId().ResultOr(0)); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(toMove.Path, Encoding.UTF8, "text/plain"); - return response; + return Content(toMove.Path, "text/plain", Encoding.UTF8); } /// @@ -1612,15 +1640,13 @@ namespace Umbraco.Web.Editors /// /// [EnsureUserPermissionForContent("copy.ParentId", 'C')] - public HttpResponseMessage PostCopy(MoveOrCopy copy) + public IActionResult PostCopy(MoveOrCopy copy) { var toCopy = ValidateMoveOrCopy(copy); - var c = Services.ContentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, Security.GetUserId().ResultOr(0)); + var c = _contentService.Copy(toCopy, copy.ParentId, copy.RelateToOriginal, copy.Recursive, _webSecurity.GetUserId().ResultOr(0)); - var response = Request.CreateResponse(HttpStatusCode.OK); - response.Content = new StringContent(c.Path, Encoding.UTF8, "text/plain"); - return response; + return Content(c.Path, "text/plain", Encoding.UTF8); } /// @@ -1629,10 +1655,10 @@ namespace Umbraco.Web.Editors /// The content and variants to unpublish /// [EnsureUserPermissionForContent("model.Id", 'Z')] - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] public ContentItemDisplay PostUnpublish(UnpublishContent model) { - var foundContent = GetObjectFromRequest(() => Services.ContentService.GetById(model.Id)); + var foundContent = GetObjectFromRequest(() => _contentService.GetById(model.Id)); if (foundContent == null) HandleContentNotFound(model.Id); @@ -1641,20 +1667,20 @@ namespace Umbraco.Web.Editors if (model.Cultures.Length == 0 || model.Cultures.Length == languageCount) { //this means that the entire content item will be unpublished - var unpublishResult = Services.ContentService.Unpublish(foundContent, userId: Security.GetUserId().ResultOr(0)); + var unpublishResult = _contentService.Unpublish(foundContent, userId: _webSecurity.GetUserId().ResultOr(0)); var content = MapToDisplay(foundContent); if (!unpublishResult.Success) { AddCancelMessage(content); - throw new HttpResponseException(Request.CreateValidationErrorResponse(content)); + throw HttpResponseException.CreateValidationErrorResponse(content); } else { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentUnpublished")); + _localizedTextService.Localize("content/unpublish"), + _localizedTextService.Localize("speechBubbles/contentUnpublished")); return content; } } @@ -1664,7 +1690,7 @@ namespace Umbraco.Web.Editors var results = new Dictionary(); foreach (var c in model.Cultures) { - var result = Services.ContentService.Unpublish(foundContent, culture: c, userId: Security.GetUserId().ResultOr(0)); + var result = _contentService.Unpublish(foundContent, culture: c, userId: _webSecurity.GetUserId().ResultOr(0)); results[c] = result; if (result.Result == PublishResultType.SuccessUnpublishMandatoryCulture) { @@ -1679,8 +1705,8 @@ namespace Umbraco.Web.Editors if (results.Any(x => x.Value.Result == PublishResultType.SuccessUnpublishMandatoryCulture)) { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentMandatoryCultureUnpublished")); + _localizedTextService.Localize("content/unpublish"), + _localizedTextService.Localize("speechBubbles/contentMandatoryCultureUnpublished")); return content; } @@ -1688,8 +1714,8 @@ namespace Umbraco.Web.Editors foreach (var r in results) { content.AddSuccessNotification( - Services.TextService.Localize("content/unpublish"), - Services.TextService.Localize("speechBubbles/contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); + _localizedTextService.Localize("content/unpublish"), + _localizedTextService.Localize("speechBubbles/contentCultureUnpublished", new[] { _allLangs.Value[r.Key].CultureName })); } return content; @@ -1699,7 +1725,7 @@ namespace Umbraco.Web.Editors public ContentDomainsAndCulture GetCultureAndDomains(int id) { - var nodeDomains = Services.DomainService.GetAssignedDomains(id, true).ToArray(); + var nodeDomains = _domainService.GetAssignedDomains(id, true).ToArray(); var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); var domains = nodeDomains.Where(d => !d.IsWildcard).Select(d => new DomainDisplay(d.DomainName, d.LanguageId.GetValueOrDefault(0))); return new ContentDomainsAndCulture @@ -1710,45 +1736,40 @@ namespace Umbraco.Web.Editors } [HttpPost] - public DomainSave PostSaveLanguageAndDomains(DomainSave model) + public ActionResult PostSaveLanguageAndDomains(DomainSave model) { foreach (var domain in model.Domains) { try { - var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, Request.RequestUri); + var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, new Uri(Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute)); } catch (UriFormatException) { - var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); - throw new HttpResponseException(response); + throw HttpResponseException.CreateValidationErrorResponse(_localizedTextService.Localize("assignDomain/invalidDomain")); } } - var node = Services.ContentService.GetById(model.NodeId); + var node = _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); + HttpContext.SetReasonPhrase("Node Not Found."); + return NotFound("There is no content node with id {model.NodeId}."); } - var permission = Services.UserService.GetPermissions(Security.CurrentUser, node.Path); + var permission = _userService.GetPermissions(_webSecurity.CurrentUser, node.Path); if (permission.AssignedPermissions.Contains(ActionAssignDomain.ActionLetter.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); + HttpContext.SetReasonPhrase("Permission Denied."); + return BadRequest("You do not have permission to assign domains on that node."); } model.Valid = true; - var domains = Services.DomainService.GetAssignedDomains(model.NodeId, true).ToArray(); - var languages = Services.LocalizationService.GetAllLanguages().ToArray(); + var domains = _domainService.GetAssignedDomains(model.NodeId, true).ToArray(); + var languages = _localizationService.GetAllLanguages().ToArray(); var language = model.Language > 0 ? languages.FirstOrDefault(l => l.Id == model.Language) : null; // process wildcard @@ -1769,13 +1790,11 @@ namespace Umbraco.Web.Editors }; } - var saveAttempt = Services.DomainService.Save(wildcard); + var saveAttempt = _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); + HttpContext.SetReasonPhrase(saveAttempt.Result.Result.ToString()); + return BadRequest("Saving domain failed"); } } else @@ -1783,7 +1802,7 @@ namespace Umbraco.Web.Editors var wildcard = domains.FirstOrDefault(d => d.IsWildcard); if (wildcard != null) { - Services.DomainService.Delete(wildcard); + _domainService.Delete(wildcard); } } @@ -1791,7 +1810,7 @@ namespace Umbraco.Web.Editors // 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); + _domainService.Delete(domain); } var names = new List(); @@ -1816,23 +1835,23 @@ namespace Umbraco.Web.Editors if (domain != null) { domain.LanguageId = language.Id; - Services.DomainService.Save(domain); + _domainService.Save(domain); } - else if (Services.DomainService.Exists(domainModel.Name)) + else if (_domainService.Exists(domainModel.Name)) { domainModel.Duplicate = true; - var xdomain = Services.DomainService.GetByName(domainModel.Name); + var xdomain = _domainService.GetByName(domainModel.Name); var xrcid = xdomain.RootContentId; if (xrcid.HasValue) { - var xcontent = Services.ContentService.GetById(xrcid.Value); + var xcontent = _contentService.GetById(xrcid.Value); var xnames = new List(); while (xcontent != null) { xnames.Add(xcontent.Name); if (xcontent.ParentId < -1) xnames.Add("Recycle Bin"); - xcontent = Services.ContentService.GetParent(xcontent); + xcontent = _contentService.GetParent(xcontent); } xnames.Reverse(); domainModel.Other = "/" + string.Join("/", xnames); @@ -1846,13 +1865,11 @@ namespace Umbraco.Web.Editors LanguageId = domainModel.Lang, RootContentId = model.NodeId }; - var saveAttempt = Services.DomainService.Save(newDomain); + var saveAttempt = _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); + HttpContext.SetReasonPhrase(saveAttempt.Result.Result.ToString()); + return BadRequest("Saving new domain failed"); } } } @@ -1966,7 +1983,7 @@ namespace Umbraco.Web.Editors } else // set: update if different { - var template = Services.FileService.GetTemplate(contentSave.TemplateAlias); + var template = _fileService.GetTemplate(contentSave.TemplateAlias); if (template == null) { //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); @@ -1991,7 +2008,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var contentService = Services.ContentService; + var contentService = _contentService; var toMove = contentService.GetById(model.Id); if (toMove == null) { @@ -2002,9 +2019,8 @@ namespace Umbraco.Web.Editors //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"))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.Localize("moveOrCopy/notAllowedAtRoot")); } } else @@ -2015,22 +2031,20 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var parentContentType = Services.ContentTypeService.Get(parent.ContentTypeId); + var parentContentType = _contentTypeService.Get(parent.ContentTypeId); //check if the item is allowed under this one if (parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { - throw new HttpResponseException( - Request.CreateNotificationValidationErrorResponse( - Services.TextService.Localize("moveOrCopy/notAllowedByContentType"))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.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"))); + throw HttpResponseException.CreateNotificationValidationErrorResponse( + _localizedTextService.Localize("moveOrCopy/notAllowedByPath")); } } @@ -2099,16 +2113,16 @@ namespace Umbraco.Web.Editors { //either invariant single publish, or bulk publish where all statuses are already published display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editContentPublishedText")); + _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles/editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), - Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2124,20 +2138,20 @@ namespace Umbraco.Web.Editors if (successfulCultures == null) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), totalStatusCount > 1 - ? Services.TextService.Localize("speechBubbles/editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) - : Services.TextService.Localize("speechBubbles/editContentPublishedText")); + ? _localizedTextService.Localize("speechBubbles/editMultiContentPublishedText", new[] { itemCount.ToInvariantString() }) + : _localizedTextService.Localize("speechBubbles/editContentPublishedText")); } else { foreach (var c in successfulCultures) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editContentPublishedHeader"), + _localizedTextService.Localize("speechBubbles/editContentPublishedHeader"), totalStatusCount > 1 - ? Services.TextService.Localize("speechBubbles/editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) - : Services.TextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); + ? _localizedTextService.Localize("speechBubbles/editMultiVariantPublishedText", new[] { itemCount.ToInvariantString(), _allLangs.Value[c].CultureName }) + : _localizedTextService.Localize("speechBubbles/editVariantPublishedText", new[] { _allLangs.Value[c].CultureName })); } } } @@ -2147,8 +2161,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedByParent", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedByParent", new[] { names }).Trim()); } break; @@ -2164,8 +2178,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedAwaitingRelease", new[] { names }).Trim()); } break; @@ -2174,8 +2188,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedExpired", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedExpired", new[] { names }).Trim()); } break; @@ -2184,8 +2198,8 @@ namespace Umbraco.Web.Editors //TODO: This doesn't take into account variations with the successfulCultures param var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedIsTrashed", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedIsTrashed", new[] { names }).Trim()); } break; @@ -2195,8 +2209,8 @@ namespace Umbraco.Web.Editors { var names = string.Join(", ", status.Select(x => $"'{x.Content.Name}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedInvalid", new[] { names }).Trim()); } else @@ -2205,8 +2219,8 @@ namespace Umbraco.Web.Editors { var names = string.Join(", ", status.Select(x => $"'{(x.Content.ContentType.VariesByCulture() ? x.Content.GetCultureName(c) : x.Content.Name)}'")); display.AddWarningNotification( - Services.TextService.Localize("publish"), - Services.TextService.Localize("publish/contentPublishedFailedInvalid", + _localizedTextService.Localize("publish"), + _localizedTextService.Localize("publish/contentPublishedFailedInvalid", new[] { names }).Trim()); } } @@ -2214,7 +2228,7 @@ namespace Umbraco.Web.Editors break; case PublishResultType.FailedPublishMandatoryCultureMissing: display.AddWarningNotification( - Services.TextService.Localize("publish"), + _localizedTextService.Localize("publish"), "publish/contentPublishedFailedByCulture"); break; default: @@ -2230,30 +2244,30 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content) { - var display = Mapper.Map(content, context => + var display = _umbracoMapper.Map(content, context => { - context.Items["CurrentUser"] = Security.CurrentUser; + context.Items["CurrentUser"] = _webSecurity.CurrentUser; }); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } [EnsureUserPermissionForContent("contentId", ActionBrowse.ActionLetter)] - public IEnumerable GetNotificationOptions(int contentId) + public ActionResult> GetNotificationOptions(int contentId) { var notifications = new List(); - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + if (contentId <= 0) return NotFound(); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + var content = _contentService.GetById(contentId); + if (content == null) return NotFound(); - var userNotifications = Services.NotificationService.GetUserNotifications(Security.CurrentUser, content.Path).ToList(); + var userNotifications = _notificationService.GetUserNotifications(_webSecurity.CurrentUser, content.Path).ToList(); - foreach (var a in Current.Actions.Where(x => x.ShowInNotifier)) + foreach (var a in _actionCollection.Where(x => x.ShowInNotifier)) { var n = new NotifySetting { - Name = Services.TextService.Localize("actions", a.Alias), + Name = _localizedTextService.Localize("actions", a.Alias), Checked = userNotifications.FirstOrDefault(x => x.Action == a.Letter.ToString()) != null, NotifyCode = a.Letter.ToString() }; @@ -2263,13 +2277,15 @@ namespace Umbraco.Web.Editors return notifications; } - public void PostNotificationOptions(int contentId, [FromUri] string[] notifyOptions) + public IActionResult PostNotificationOptions(int contentId, [FromQuery] string[] notifyOptions) { - if (contentId <= 0) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); - var content = Services.ContentService.GetById(contentId); - if (content == null) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + if (contentId <= 0) return NotFound(); + var content = _contentService.GetById(contentId); + if (content == null) return NotFound(); - Services.NotificationService.SetNotifications(Security.CurrentUser, content, notifyOptions); + _notificationService.SetNotifications(_webSecurity.CurrentUser, content, notifyOptions); + + return NoContent(); } [HttpGet] @@ -2278,7 +2294,7 @@ namespace Umbraco.Web.Editors var rollbackVersions = new List(); var writerIds = new HashSet(); - var versions = Services.ContentService.GetVersionsSlim(contentId, 0, 50); + var versions = _contentService.GetVersionsSlim(contentId, 0, 50); //Not all nodes are variants & thus culture can be null if (culture != null) @@ -2304,7 +2320,7 @@ namespace Umbraco.Web.Editors writerIds.Add(version.WriterId); } - var users = Services.UserService + var users = _userService .GetUsersById(writerIds.ToArray()) .ToDictionary(x => x.Id, x => x.Name); @@ -2320,7 +2336,7 @@ namespace Umbraco.Web.Editors [HttpGet] public ContentVariantDisplay GetRollbackVersion(int versionId, string culture = null) { - var version = Services.ContentService.GetVersion(versionId); + var version = _contentService.GetVersion(versionId); var content = MapToDisplay(version); return culture == null @@ -2330,12 +2346,12 @@ namespace Umbraco.Web.Editors [EnsureUserPermissionForContent("contentId", ActionRollback.ActionLetter)] [HttpPost] - public HttpResponseMessage PostRollbackContent(int contentId, int versionId, string culture = "*") + public IActionResult PostRollbackContent(int contentId, int versionId, string culture = "*") { - var rollbackResult = Services.ContentService.Rollback(contentId, versionId, culture, Security.GetUserId().ResultOr(0)); + var rollbackResult = _contentService.Rollback(contentId, versionId, culture, _webSecurity.GetUserId().ResultOr(0)); if (rollbackResult.Success) - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); var notificationModel = new SimpleNotificationModel(); @@ -2347,37 +2363,37 @@ namespace Umbraco.Web.Editors case OperationResultType.NoOperation: default: notificationModel.AddErrorNotification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), + _localizedTextService.Localize("speechBubbles/operationFailedHeader"), null); // TODO: There is no specific failed to save error message AFAIK break; case OperationResultType.FailedCancelledByEvent: notificationModel.AddErrorNotification( - Services.TextService.Localize("speechBubbles/operationCancelledHeader"), - Services.TextService.Localize("speechBubbles/operationCancelledText")); + _localizedTextService.Localize("speechBubbles/operationCancelledHeader"), + _localizedTextService.Localize("speechBubbles/operationCancelledText")); break; } - return Request.CreateValidationErrorResponse(notificationModel); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpGet] - public HttpResponseMessage GetPublicAccess(int contentId) + public IActionResult GetPublicAccess(int contentId) { - var content = Services.ContentService.GetById(contentId); + var content = _contentService.GetById(contentId); if (content == null) { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + return NotFound(); } - var entry = Services.PublicAccessService.GetEntryForContent(content); + var entry = _publicAccessService.GetEntryForContent(content); if (entry == null || entry.ProtectedNodeId != content.Id) { - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } - var loginPageEntity = Services.EntityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); - var errorPageEntity = Services.EntityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); + var loginPageEntity = _entityService.Get(entry.LoginNodeId, UmbracoObjectTypes.Document); + var errorPageEntity = _entityService.Get(entry.NoAccessNodeId, UmbracoObjectTypes.Document); // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) @@ -2386,44 +2402,44 @@ namespace Umbraco.Web.Editors .Select(rule => rule.RuleValue).ToArray(); var members = usernames - .Select(username => Services.MemberService.GetByUsername(username)) + .Select(username => _memberService.GetByUsername(username)) .Where(member => member != null) - .Select(Mapper.Map) + .Select(_umbracoMapper.Map) .ToArray(); - var allGroups = Services.MemberGroupService.GetAll().ToArray(); + var allGroups = _memberGroupService.GetAll().ToArray(); var groups = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) .Select(rule => allGroups.FirstOrDefault(g => g.Name == rule.RuleValue)) .Where(memberGroup => memberGroup != null) - .Select(Mapper.Map) + .Select(_umbracoMapper.Map) .ToArray(); - return Request.CreateResponse(HttpStatusCode.OK, new PublicAccess + return Ok(new PublicAccess { Members = members, Groups = groups, - LoginPage = loginPageEntity != null ? Mapper.Map(loginPageEntity) : null, - ErrorPage = errorPageEntity != null ? Mapper.Map(errorPageEntity) : null + LoginPage = loginPageEntity != null ? _umbracoMapper.Map(loginPageEntity) : null, + ErrorPage = errorPageEntity != null ? _umbracoMapper.Map(errorPageEntity) : null }); } // set up public access using role based access [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] groups, [FromUri]string[] usernames, int loginPageId, int errorPageId) + public IActionResult PostPublicAccess(int contentId, [FromQuery]string[] groups, [FromQuery]string[] usernames, int loginPageId, int errorPageId) { if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + return BadRequest(); } - var content = Services.ContentService.GetById(contentId); - var loginPage = Services.ContentService.GetById(loginPageId); - var errorPage = Services.ContentService.GetById(errorPageId); + var content = _contentService.GetById(contentId); + var loginPage = _contentService.GetById(loginPageId); + var errorPage = _contentService.GetById(errorPageId); if (content == null || loginPage == null || errorPage == null) { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); + return BadRequest(); } var isGroupBased = groups != null && groups.Any(); @@ -2434,7 +2450,7 @@ namespace Umbraco.Web.Editors ? Constants.Conventions.PublicAccess.MemberRoleRuleType : Constants.Conventions.PublicAccess.MemberUsernameRuleType; - var entry = Services.PublicAccessService.GetEntryForContent(content); + var entry = _publicAccessService.GetEntryForContent(content); if (entry == null || entry.ProtectedNodeId != content.Id) { @@ -2471,30 +2487,34 @@ namespace Umbraco.Web.Editors } } - return Services.PublicAccessService.Save(entry).Success - ? Request.CreateResponse(HttpStatusCode.OK) - : Request.CreateResponse(HttpStatusCode.InternalServerError); + return _publicAccessService.Save(entry).Success + ? (IActionResult) Ok() + : Problem(); } [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage RemovePublicAccess(int contentId) + public IActionResult RemovePublicAccess(int contentId) { - var content = Services.ContentService.GetById(contentId); + var content = _contentService.GetById(contentId); if (content == null) { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound)); + return NotFound(); } - var entry = Services.PublicAccessService.GetEntryForContent(content); + var entry = _publicAccessService.GetEntryForContent(content); if (entry == null) { - return Request.CreateResponse(HttpStatusCode.OK); + return Ok(); } - return Services.PublicAccessService.Delete(entry).Success - ? Request.CreateResponse(HttpStatusCode.OK) - : Request.CreateResponse(HttpStatusCode.InternalServerError); + return _publicAccessService.Delete(entry).Success + ? (IActionResult) Ok() + : Problem(); } } + + internal class _publishedUrlProvider + { + } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs new file mode 100644 index 0000000000..bf23c1295f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentControllerBase.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Composing; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Controllers; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + /// + /// An abstract base controller used for media/content/members to try to reduce code replication. + /// + //[JsonDateTimeFormatAttribute] //TODO Reintroduce + public abstract class ContentControllerBase : BackOfficeNotificationsController + { + protected ICultureDictionary CultureDictionary { get; } + protected ILogger Logger { get; } + protected IShortStringHelper ShortStringHelper { get; } + protected EventMessages EventMessages { get; } + protected ILocalizedTextService LocalizedTextService { get; } + + protected ContentControllerBase( + ICultureDictionary cultureDictionary, + ILogger logger, + IShortStringHelper shortStringHelper, + EventMessages eventMessages, + ILocalizedTextService localizedTextService) + { + CultureDictionary = cultureDictionary; + Logger = logger; + ShortStringHelper = shortStringHelper; + EventMessages = eventMessages; + LocalizedTextService = localizedTextService; + } + + protected NotFoundObjectResult HandleContentNotFound(object id, bool throwException = true) + { + ModelState.AddModelError("id", $"content with id: {id} was not found"); + var errorResponse = NotFound(ModelState); + if (throwException) + { + throw new HttpResponseException(errorResponse); + } + return errorResponse; + } + + /// + /// Maps the dto property values to the persisted model + /// + internal void MapPropertyValuesForPersistence( + TSaved contentItem, + ContentPropertyCollectionDto dto, + Func getPropertyValue, + Action savePropertyValue, + string culture) + where TPersisted : IContentBase + where TSaved : IContentSave + { + // map the property values + foreach (var propertyDto in dto.Properties) + { + // get the property editor + if (propertyDto.PropertyEditor == null) + { + Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); + continue; + } + + // get the value editor + // nothing to save/map if it is readonly + var valueEditor = propertyDto.PropertyEditor.GetValueEditor(); + if (valueEditor.IsReadOnly) continue; + + // get the property + var property = contentItem.PersistedContent.Properties[propertyDto.Alias]; + + // prepare files, if any matching property and culture + var files = contentItem.UploadedFiles + .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) + .ToArray(); + + foreach (var file in files) + file.FileName = file.FileName.ToSafeFileName(ShortStringHelper); + + // create the property data for the property editor + var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration) + { + ContentKey = contentItem.PersistedContent.Key, + PropertyTypeKey = property.PropertyType.Key, + Files = files + }; + + // let the editor convert the value that was received, deal with files, etc + var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property)); + + // set the value - tags are special + var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute(); + if (tagAttribute != null) + { + var tagConfiguration = ConfigurationEditor.ConfigurationAs(propertyDto.DataType.Configuration); + if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter; + var tagCulture = property.PropertyType.VariesByCulture() ? culture : null; + property.SetTagsValue(value, tagConfiguration, tagCulture); + } + else + savePropertyValue(contentItem, property, value); + } + } + + protected virtual void HandleInvalidModelState(IErrorModel display) + { + //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + if (!ModelState.IsValid) + { + display.Errors = ModelState.ToErrorDictionary(); + throw HttpResponseException.CreateValidationErrorResponse(display); + } + } + + /// + /// A helper method to attempt to get the instance from the request storage if it can be found there, + /// otherwise gets it from the callback specified + /// + /// + /// + /// + /// + /// This is useful for when filters have already looked up a persisted entity and we don't want to have + /// to look it up again. + /// + protected TPersisted GetObjectFromRequest(Func getFromService) + { + //checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return + // it from the callback + return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null + ? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()] + : getFromService(); + } + + /// + /// Returns true if the action passed in means we need to create something new + /// + /// + /// + internal static bool IsCreatingAction(ContentSaveAction action) + { + return (action.ToString().EndsWith("New")); + } + + protected void AddCancelMessage(INotificationModel display, + string header = "speechBubbles/operationCancelledHeader", + string message = "speechBubbles/operationCancelledText", + bool localizeHeader = true, + bool localizeMessage = true, + string[] headerParams = null, + string[] messageParams = null) + { + //if there's already a default event message, don't add our default one + var msgs = EventMessages; + if (msgs != null && msgs.GetAll().Any(x => x.IsDefaultEventMessage)) return; + + display.AddWarningNotification( + localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header, + localizeMessage ? LocalizedTextService.Localize(message, messageParams): message); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs index 14fc25cfeb..a302f7f7cf 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ExamineManagementController.cs @@ -165,7 +165,7 @@ namespace Umbraco.Web.BackOffice.Controllers _logger.Error(ex, "An error occurred rebuilding index"); var response = new ConflictObjectResult("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {ex}"); - SetReasonPhrase(response, "Could Not Rebuild"); + HttpContext.SetReasonPhrase("Could Not Rebuild"); return response; } } @@ -212,7 +212,7 @@ namespace Umbraco.Web.BackOffice.Controllers return new OkResult(); var response1 = new BadRequestObjectResult($"No searcher found with name = {searcherName}"); - SetReasonPhrase(response1, "Searcher Not Found"); + HttpContext.SetReasonPhrase("Searcher Not Found"); return response1; } @@ -222,7 +222,7 @@ namespace Umbraco.Web.BackOffice.Controllers return new OkResult(); var response = new BadRequestObjectResult($"The index {index.Name} cannot be rebuilt because it does not have an associated {typeof(IIndexPopulator)}"); - SetReasonPhrase(response, "Index cannot be rebuilt"); + HttpContext.SetReasonPhrase("Index cannot be rebuilt"); return response; } @@ -237,21 +237,10 @@ namespace Umbraco.Web.BackOffice.Controllers } var response = new BadRequestObjectResult($"No index found with name = {indexName}"); - SetReasonPhrase(response, "Index Not Found"); + HttpContext.SetReasonPhrase("Index Not Found"); return response; } - private void SetReasonPhrase(IActionResult response, string reasonPhrase) - { - //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body - // https://github.com/aspnet/HttpAbstractions/issues/395 - var httpResponseFeature = HttpContext.Features.Get(); - if (!(httpResponseFeature is null)) - { - httpResponseFeature.ReasonPhrase = reasonPhrase; - } - } - private void Indexer_IndexOperationComplete(object sender, EventArgs e) { var indexer = (IIndex)sender; diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs new file mode 100644 index 0000000000..ce515886f0 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/ContentModelValidator.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Extensions; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// A base class purely used for logging without generics + /// + internal abstract class ContentModelValidator + { + protected IWebSecurity WebSecurity { get; } + protected ILogger Logger { get; } + + protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity) + { + Logger = logger ?? throw new ArgumentNullException(nameof(logger)); + WebSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + } + } + + /// + /// A validation helper class used with ContentItemValidationFilterAttribute to be shared between content, media, etc... + /// + /// + /// + /// + /// + /// If any severe errors occur then the response gets set to an error and execution will not continue. Property validation + /// errors will just be added to the ModelState. + /// + internal abstract class ContentModelValidator: ContentModelValidator + where TPersisted : class, IContentBase + where TModelSave: IContentSave + where TModelWithProperties : IContentProperties + { + private readonly ILocalizedTextService _textService; + + protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity) + { + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + } + + /// + /// Ensure the content exists + /// + /// + /// + /// + public virtual bool ValidateExistingContent(TModelSave postedItem, ActionExecutingContext actionContext) + { + var persistedContent = postedItem.PersistedContent; + if (persistedContent == null) + { + actionContext.Result = new NotFoundObjectResult("content was not found"); + return false; + } + + return true; + } + + /// + /// Ensure all of the ids in the post are valid + /// + /// + /// + /// + /// + public virtual bool ValidateProperties(TModelSave model, IContentProperties modelWithProperties, ActionExecutingContext actionContext) + { + var persistedContent = model.PersistedContent; + return ValidateProperties(modelWithProperties.Properties.ToList(), persistedContent.Properties.ToList(), actionContext); + } + + /// + /// This validates that all of the posted properties exist on the persisted entity + /// + /// + /// + /// + /// + protected bool ValidateProperties(List postedProperties, List persistedProperties, ActionExecutingContext actionContext) + { + foreach (var p in postedProperties) + { + if (persistedProperties.Any(property => property.Alias == p.Alias) == false) + { + // TODO: Do we return errors here ? If someone deletes a property whilst their editing then should we just + //save the property data that remains? Or inform them they need to reload... not sure. This problem exists currently too i think. + + var message = $"property with alias: {p.Alias} was not found"; + actionContext.Result = new NotFoundObjectResult(new InvalidOperationException(message)); + return false; + } + + } + return true; + } + + /// + /// Validates the data for each property + /// + /// + /// + /// + /// + /// + /// + /// All property data validation goes into the model state with a prefix of "Properties" + /// + public virtual bool ValidatePropertiesData( + TModelSave model, + TModelWithProperties modelWithProperties, + ContentPropertyCollectionDto dto, + ModelStateDictionary modelState) + { + var properties = modelWithProperties.Properties.ToDictionary(x => x.Alias, x => x); + + // Retrieve default messages used for required and regex validatation. We'll replace these + // if set with custom ones if they've been provided for a given property. + var requiredDefaultMessages = new[] + { + _textService.Localize("validation", "invalidNull"), + _textService.Localize("validation", "invalidEmpty") + }; + var formatDefaultMessages = new[] + { + _textService.Localize("validation", "invalidPattern"), + }; + + foreach (var p in dto.Properties) + { + var editor = p.PropertyEditor; + + if (editor == null) + { + var message = $"Could not find property editor \"{p.DataType.EditorAlias}\" for property with id {p.Id}."; + + Logger.Warn(message); + continue; + } + + //get the posted value for this property, this may be null in cases where the property was marked as readonly which means + //the angular app will not post that value. + if (!properties.TryGetValue(p.Alias, out var postedProp)) + continue; + + var postedValue = postedProp.Value; + + ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages); + + } + + return modelState.IsValid; + } + + /// + /// Validates a property's value and adds the error to model state if found + /// + /// + /// + /// + /// + /// + /// + /// + /// + protected virtual void ValidatePropertyValue( + TModelSave model, + TModelWithProperties modelWithProperties, + IDataEditor editor, + ContentPropertyDto property, + object postedValue, + ModelStateDictionary modelState, + string[] requiredDefaultMessages, + string[] formatDefaultMessages) + { + var valueEditor = editor.GetValueEditor(property.DataType.Configuration); + foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp)) + { + // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). + if (property.IsRequired && !string.IsNullOrWhiteSpace(property.IsRequiredMessage) && requiredDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + r.ErrorMessage = property.IsRequiredMessage; + } + + if (!string.IsNullOrWhiteSpace(property.ValidationRegExp) && !string.IsNullOrWhiteSpace(property.ValidationRegExpMessage) && formatDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + r.ErrorMessage = property.ValidationRegExpMessage; + } + + modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment); + } + } + } +} diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs similarity index 93% rename from src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs rename to src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs index 3e2b0e5cfa..9cf4ff2a09 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveModelValidator.cs @@ -4,7 +4,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Security; -namespace Umbraco.Web.Editors.Filters +namespace Umbraco.Web.BackOffice.Filters { /// /// Validator for diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs new file mode 100644 index 0000000000..9a9d22a854 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.Actions; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Validates the incoming model along with if the user is allowed to perform the + /// operation + /// + internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute + { + public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) + { + } + + private sealed class ContentSaveValidationFilter : IActionFilter + { + private readonly IContentService _contentService; + private readonly IEntityService _entityService; + private readonly ILogger _logger; + private readonly ILocalizedTextService _textService; + private readonly IUserService _userService; + private readonly IWebSecurity _webSecurity; + + + public ContentSaveValidationFilter(ILogger logger, IWebSecurity webSecurity, + ILocalizedTextService textService, IContentService contentService, IUserService userService, + IEntityService entityService) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var model = (ContentItemSave) context.ActionArguments["contentItem"]; + var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService); + + if (!ValidateAtLeastOneVariantIsBeingSaved(model, context)) return; + if (!contentItemValidator.ValidateExistingContent(model, context)) return; + if (!ValidateUserAccess(model, context, _webSecurity)) return; + + //validate for each variant that is being updated + foreach (var variant in model.Variants.Where(x => x.Save)) + { + if (contentItemValidator.ValidateProperties(model, variant, context)) + contentItemValidator.ValidatePropertiesData(model, variant, variant.PropertyCollectionDto, + context.ModelState); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + /// + /// If there are no variants tagged for Saving, then this is an invalid request + /// + /// + /// + /// + private bool ValidateAtLeastOneVariantIsBeingSaved(ContentItemSave contentItem, + ActionExecutingContext actionContext) + { + if (!contentItem.Variants.Any(x => x.Save)) + { + actionContext.Result = new NotFoundObjectResult("No variants flagged for saving"); + return false; + } + + return true; + } + + /// + /// Checks if the user has access to post a content item based on whether it's being created or saved. + /// + /// + /// + /// + private bool ValidateUserAccess(ContentItemSave contentItem, ActionExecutingContext actionContext, + IWebSecurity webSecurity) + { + // We now need to validate that the user is allowed to be doing what they are doing. + // Based on the action we need to check different permissions. + // Then if it is new, we need to lookup those permissions on the parent! + + var permissionToCheck = new List(); + IContent contentToCheck = null; + int contentIdToCheck; + switch (contentItem.Action) + { + case ContentSaveAction.Save: + permissionToCheck.Add(ActionUpdate.ActionLetter); + contentToCheck = contentItem.PersistedContent; + contentIdToCheck = contentToCheck.Id; + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishWithDescendants: + case ContentSaveAction.PublishWithDescendantsForce: + permissionToCheck.Add(ActionPublish.ActionLetter); + contentToCheck = contentItem.PersistedContent; + contentIdToCheck = contentToCheck.Id; + break; + case ContentSaveAction.SendPublish: + permissionToCheck.Add(ActionToPublish.ActionLetter); + contentToCheck = contentItem.PersistedContent; + contentIdToCheck = contentToCheck.Id; + break; + case ContentSaveAction.Schedule: + permissionToCheck.Add(ActionUpdate.ActionLetter); + permissionToCheck.Add(ActionToPublish.ActionLetter); + contentToCheck = contentItem.PersistedContent; + contentIdToCheck = contentToCheck.Id; + break; + case ContentSaveAction.SaveNew: + //Save new requires ActionNew + + permissionToCheck.Add(ActionNew.ActionLetter); + + if (contentItem.ParentId != Constants.System.Root) + { + contentToCheck = _contentService.GetById(contentItem.ParentId); + contentIdToCheck = contentToCheck.Id; + } + else + { + contentIdToCheck = contentItem.ParentId; + } + + break; + case ContentSaveAction.SendPublishNew: + //Send new requires both ActionToPublish AND ActionNew + + permissionToCheck.Add(ActionNew.ActionLetter); + permissionToCheck.Add(ActionToPublish.ActionLetter); + if (contentItem.ParentId != Constants.System.Root) + { + contentToCheck = _contentService.GetById(contentItem.ParentId); + contentIdToCheck = contentToCheck.Id; + } + else + { + contentIdToCheck = contentItem.ParentId; + } + + break; + case ContentSaveAction.PublishNew: + case ContentSaveAction.PublishWithDescendantsNew: + case ContentSaveAction.PublishWithDescendantsForceNew: + //Publish new requires both ActionNew AND ActionPublish + // TODO: Shouldn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied + + permissionToCheck.Add(ActionNew.ActionLetter); + permissionToCheck.Add(ActionPublish.ActionLetter); + + if (contentItem.ParentId != Constants.System.Root) + { + contentToCheck = _contentService.GetById(contentItem.ParentId); + contentIdToCheck = contentToCheck.Id; + } + else + { + contentIdToCheck = contentItem.ParentId; + } + + break; + case ContentSaveAction.ScheduleNew: + + permissionToCheck.Add(ActionNew.ActionLetter); + permissionToCheck.Add(ActionUpdate.ActionLetter); + permissionToCheck.Add(ActionPublish.ActionLetter); + + if (contentItem.ParentId != Constants.System.Root) + { + contentToCheck = _contentService.GetById(contentItem.ParentId); + contentIdToCheck = contentToCheck.Id; + } + else + { + contentIdToCheck = contentItem.ParentId; + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + + ContentPermissionsHelper.ContentAccess accessResult; + if (contentToCheck != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + actionContext.HttpContext.Items[typeof(IContent).ToString()] = contentItem; + + accessResult = ContentPermissionsHelper.CheckPermissions( + contentToCheck, webSecurity.CurrentUser, + _userService, _entityService, permissionToCheck.ToArray()); + } + else + { + accessResult = ContentPermissionsHelper.CheckPermissions( + contentIdToCheck, webSecurity.CurrentUser, + _userService, _contentService, _entityService, + out contentToCheck, + permissionToCheck.ToArray()); + if (contentToCheck != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + actionContext.HttpContext.Items[typeof(IContent).ToString()] = contentToCheck; + } + } + + if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound) + { + actionContext.Result = new NotFoundResult(); + } + + if (accessResult != ContentPermissionsHelper.ContentAccess.Granted) + { + actionContext.Result = new ForbidResult(); + } + + return accessResult == ContentPermissionsHelper.ContentAccess.Granted; + } + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs new file mode 100644 index 0000000000..398263975d --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForContentAttribute.cs @@ -0,0 +1,209 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Web.Actions; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Auth filter to check if the current user has access to the content item (by id). + /// + /// + /// This first checks if the user can access this based on their start node, and then checks node permissions + /// By default the permission that is checked is browse but this can be specified in the ctor. + /// NOTE: This cannot be an auth filter because that happens too soon and we don't have access to the action params. + /// + public sealed class EnsureUserPermissionForContentAttribute : TypeFilterAttribute + { + + /// + /// This constructor will only be able to test the start node access + /// + public EnsureUserPermissionForContentAttribute(int nodeId) + : base(typeof(EnsureUserPermissionForContentFilter)) + { + Arguments = new object[] + { + nodeId, null, null + }; + } + + + public EnsureUserPermissionForContentAttribute(int nodeId, char permissionToCheck) + : base(typeof(EnsureUserPermissionForContentFilter)) + { + Arguments = new object[] + { + nodeId, null, permissionToCheck + }; + } + + public EnsureUserPermissionForContentAttribute(string paramName) + : base(typeof(EnsureUserPermissionForContentFilter)) + { + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) + throw new ArgumentException("Value can't be empty.", nameof(paramName)); + + Arguments = new object[] + { + null, paramName, ActionBrowse.ActionLetter + }; + } + + + public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) + : base(typeof(EnsureUserPermissionForContentFilter)) + { + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) + throw new ArgumentException("Value can't be empty.", nameof(paramName)); + + Arguments = new object[] + { + null, paramName, permissionToCheck + }; + } + + private sealed class EnsureUserPermissionForContentFilter : IActionFilter + { + private readonly int? _nodeId; + private readonly IWebSecurity _webSecurity; + private readonly IEntityService _entityService; + private readonly IUserService _userService; + private readonly IContentService _contentService; + private readonly string _paramName; + private readonly char? _permissionToCheck; + + + + public EnsureUserPermissionForContentFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IUserService userService, + IContentService contentService, + int? nodeId, string paramName, char? permissionToCheck) + { + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + + _paramName = paramName; + if (permissionToCheck.HasValue) + { + _permissionToCheck = permissionToCheck.Value; + } + + + if (nodeId.HasValue) + { + _nodeId = nodeId.Value; + } + } + + + + public void OnActionExecuting(ActionExecutingContext context) + { + if (_webSecurity.CurrentUser == null) + { + //not logged in + throw new HttpResponseException(HttpStatusCode.Unauthorized); + } + + int nodeId; + if (_nodeId.HasValue == false) + { + var parts = _paramName.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + + if (context.ActionArguments[parts[0]] == null) + { + throw new InvalidOperationException("No argument found for the current action with the name: " + + _paramName); + } + + if (parts.Length == 1) + { + var argument = context.ActionArguments[parts[0]].ToString(); + // if the argument is an int, it will parse and can be assigned to nodeId + // if might be a udi, so check that next + // otherwise treat it as a guid - unlikely we ever get here + if (int.TryParse(argument, out int parsedId)) + { + nodeId = parsedId; + } + else if (UdiParser.TryParse(argument, true, out var udi)) + { + nodeId = _entityService.GetId(udi).Result; + } + else + { + Guid.TryParse(argument, out Guid key); + nodeId = _entityService.GetId(key, UmbracoObjectTypes.Document).Result; + } + } + else + { + //now we need to see if we can get the property of whatever object it is + var pType = context.ActionArguments[parts[0]].GetType(); + var prop = pType.GetProperty(parts[1]); + if (prop == null) + { + throw new InvalidOperationException( + "No argument found for the current action with the name: " + _paramName); + } + + nodeId = (int) prop.GetValue(context.ActionArguments[parts[0]]); + } + } + else + { + nodeId = _nodeId.Value; + } + + var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, + _webSecurity.CurrentUser, + _userService, + _contentService, + _entityService, + out var contentItem, + _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); + + if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) + { + context.Result = new NotFoundResult(); + return; + } + + if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) + { + context.Result = new ForbidResult(); + return; + } + + + if (contentItem != null) + { + //store the content item in request cache so it can be resolved in the controller without re-looking it up + context.HttpContext.Items[typeof(IContent).ToString()] = contentItem; + } + + } + + public void OnActionExecuted(ActionExecutedContext context) + { + + } + + + } + } +} diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs similarity index 62% rename from src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs rename to src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs index 8c20d437d7..d6863b0bef 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingContentAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingContentAttribute.cs @@ -3,12 +3,13 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Core; -using Umbraco.Web.Composing; using Umbraco.Core.Models; using Umbraco.Web.Actions; +using Umbraco.Web.Security; namespace Umbraco.Web.WebApi.Filters @@ -17,40 +18,50 @@ namespace Umbraco.Web.WebApi.Filters /// This inspects the result of the action that returns a collection of content and removes /// any item that the current user doesn't have access to /// - internal sealed class FilterAllowedOutgoingContentAttribute : FilterAllowedOutgoingMediaAttribute + internal sealed class FilterAllowedOutgoingContentAttribute : TypeFilterAttribute + { + + internal FilterAllowedOutgoingContentAttribute(Type outgoingType) + : this(outgoingType, null, ActionBrowse.ActionLetter) + { + + } + + internal FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck) + : this(outgoingType, null, permissionToCheck) + { + } + + internal FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName) + : this(outgoingType, propertyName, ActionBrowse.ActionLetter) + { + } + + internal FilterAllowedOutgoingContentAttribute(Type outgoingType, IUserService userService, IEntityService entityService) + : this(outgoingType, null, ActionBrowse.ActionLetter) + { + + } + + public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName, char permissionToCheck) + : base(typeof(FilterAllowedOutgoingContentFilter)) + { + Arguments = new object[] + { + outgoingType, propertyName, permissionToCheck + }; + } + } + internal sealed class FilterAllowedOutgoingContentFilter : FilterAllowedOutgoingMediaFilter { private readonly IUserService _userService; private readonly IEntityService _entityService; private readonly char _permissionToCheck; - public FilterAllowedOutgoingContentAttribute(Type outgoingType) - : this(outgoingType, Current.Services.UserService, Current.Services.EntityService) - { - _permissionToCheck = ActionBrowse.ActionLetter; - } - public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck) - : this(outgoingType, Current.Services.UserService, Current.Services.EntityService) - { - _permissionToCheck = permissionToCheck; - } - public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName) - : this(outgoingType, propertyName, Current.Services.UserService, Current.Services.EntityService) - { - _permissionToCheck = ActionBrowse.ActionLetter; - } - - public FilterAllowedOutgoingContentAttribute(Type outgoingType, IUserService userService, IEntityService entityService) - : base(outgoingType) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _permissionToCheck = ActionBrowse.ActionLetter; - } - - public FilterAllowedOutgoingContentAttribute(Type outgoingType, char permissionToCheck, IUserService userService, IEntityService entityService) - : base(outgoingType) + public FilterAllowedOutgoingContentFilter(Type outgoingType, string propertyName, char permissionToCheck, IUserService userService, IEntityService entityService, IWebSecurity webSecurity) + : base(entityService, webSecurity, outgoingType, propertyName) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); @@ -59,16 +70,6 @@ namespace Umbraco.Web.WebApi.Filters _permissionToCheck = permissionToCheck; } - public FilterAllowedOutgoingContentAttribute(Type outgoingType, string propertyName, IUserService userService, IEntityService entityService) - : base(outgoingType, propertyName) - { - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - _userService = userService; - _entityService = entityService; - _permissionToCheck = ActionBrowse.ActionLetter; - } - protected override void FilterItems(IUser user, IList items) { base.FilterItems(user, items); diff --git a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs similarity index 70% rename from src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs rename to src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs index 18b1a1dab9..a600f5a928 100644 --- a/src/Umbraco.Web/WebApi/Filters/FilterAllowedOutgoingMediaAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/FilterAllowedOutgoingMediaAttribute.cs @@ -2,14 +2,16 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Net.Http; -using System.Web.Http.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; -using Current = Umbraco.Web.Composing.Current; +using Umbraco.Core.Services; +using Umbraco.Web.Security; + namespace Umbraco.Web.WebApi.Filters { @@ -17,42 +19,48 @@ namespace Umbraco.Web.WebApi.Filters /// This inspects the result of the action that returns a collection of content and removes /// any item that the current user doesn't have access to /// - internal class FilterAllowedOutgoingMediaAttribute : ActionFilterAttribute + internal class FilterAllowedOutgoingMediaAttribute : TypeFilterAttribute + { + public FilterAllowedOutgoingMediaAttribute(Type outgoingType, string propertyName = null) + : base(typeof(FilterAllowedOutgoingMediaFilter)) + { + Arguments = new object[] + { + outgoingType, propertyName + }; + } + } + internal class FilterAllowedOutgoingMediaFilter : IActionFilter { private readonly Type _outgoingType; + private readonly IEntityService _entityService; + private readonly IWebSecurity _webSecurity; private readonly string _propertyName; - public FilterAllowedOutgoingMediaAttribute(Type outgoingType) + public FilterAllowedOutgoingMediaFilter(IEntityService entityService, IWebSecurity webSecurity, Type outgoingType, string propertyName) { + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + + _propertyName = propertyName; _outgoingType = outgoingType; } - public FilterAllowedOutgoingMediaAttribute(Type outgoingType, string propertyName) - : this(outgoingType) - { - _propertyName = propertyName; - } - - /// - /// Returns true so that other filters can execute along with this one - /// - public override bool AllowMultiple => true; - protected virtual int[] GetUserStartNodes(IUser user) { - return user.CalculateMediaStartNodeIds(Current.Services.EntityService); + return user.CalculateMediaStartNodeIds(_entityService); } protected virtual int RecycleBinId => Constants.System.RecycleBinMedia; - public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) + public void OnActionExecuted(ActionExecutedContext context) { - if (actionExecutedContext.Response == null) return; + if (context.Result == null) return; - var user = Composing.Current.UmbracoContext.Security.CurrentUser; + var user = _webSecurity.CurrentUser; if (user == null) return; - var objectContent = actionExecutedContext.Response.Content as ObjectContent; + var objectContent = context.Result as ObjectResult; if (objectContent != null) { var collection = GetValueFromResponse(objectContent); @@ -67,8 +75,6 @@ namespace Umbraco.Web.WebApi.Filters SetValueForResponse(objectContent, items); } } - - base.OnActionExecuted(actionExecutedContext); } protected virtual void FilterItems(IUser user, IList items) @@ -94,7 +100,7 @@ namespace Umbraco.Web.WebApi.Filters } } - private void SetValueForResponse(ObjectContent objectContent, dynamic newVal) + private void SetValueForResponse(ObjectResult objectContent, dynamic newVal) { if (TypeHelper.IsTypeAssignableFrom(_outgoingType, objectContent.Value.GetType())) { @@ -112,7 +118,7 @@ namespace Umbraco.Web.WebApi.Filters } - internal dynamic GetValueFromResponse(ObjectContent objectContent) + internal dynamic GetValueFromResponse(ObjectResult objectContent) { if (TypeHelper.IsTypeAssignableFrom(_outgoingType, objectContent.Value.GetType())) { @@ -135,5 +141,11 @@ namespace Umbraco.Web.WebApi.Filters return null; } + + + public void OnActionExecuting(ActionExecutingContext context) + { + + } } } diff --git a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 6431911a1f..4558cf348c 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Extensions; using Umbraco.Web.BackOffice.Security; namespace Umbraco.Web.BackOffice.Filters @@ -54,14 +55,7 @@ namespace Umbraco.Web.BackOffice.Filters var validateResult = await ValidateHeaders(httpContext, cookieToken); if (validateResult.Item1 == false) { - //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body - // https://github.com/aspnet/HttpAbstractions/issues/395 - var httpResponseFeature = httpContext.Features.Get(); - if (!(httpResponseFeature is null)) - { - httpResponseFeature.ReasonPhrase = validateResult.Item2; - } - + httpContext.SetReasonPhrase(validateResult.Item2); context.Result = new StatusCodeResult((int)HttpStatusCode.ExpectationFailed); return; } diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs new file mode 100644 index 0000000000..6d7b575012 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs @@ -0,0 +1,24 @@ +using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Binders +{ + internal class BlueprintItemBinder : ContentItemBinder + { + private readonly ContentService _contentService; + + public BlueprintItemBinder(UmbracoMapper umbracoMapper, ContentTypeService contentTypeService, ContentService contentService) : base(umbracoMapper, contentTypeService, contentService) + { + _contentService = contentService; + } + + protected override IContent GetExisting(ContentItemSave model) + { + return _contentService.GetBlueprintById(model.Id); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs new file mode 100644 index 0000000000..7e29e3e9d1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Models.Mapping; + +namespace Umbraco.Web.Editors.Binders +{ + /// + /// The model binder for + /// + internal class ContentItemBinder : IModelBinder + { + private readonly UmbracoMapper _umbracoMapper; + private readonly ContentTypeService _contentTypeService; + private readonly ContentService _contentService; + private readonly ContentModelBinderHelper _modelBinderHelper; + + public ContentItemBinder(UmbracoMapper umbracoMapper, ContentTypeService contentTypeService, ContentService contentService) + { + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _contentTypeService = contentTypeService ?? throw new ArgumentNullException(nameof(contentTypeService)); + _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); + _modelBinderHelper = new ContentModelBinderHelper(); + } + + /// + /// Creates the model from the request and binds it to the context + /// + /// + /// + /// + public Task BindModelAsync(ModelBindingContext bindingContext) + { + var actionContext = bindingContext.ActionContext; + + var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + if (model == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + foreach (var variant in model.Variants) + { + //map the property dto collection with the culture of the current variant + variant.PropertyCollectionDto = _umbracoMapper.Map( + model.PersistedContent, + context => + { + // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); + + //now map all of the saved values to the dto + _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + } + } + + return Task.CompletedTask; + } + + public bool BindModel(ActionContext actionContext, ModelBindingContext bindingContext) + { + var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + if (model == null) return false; + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + foreach (var variant in model.Variants) + { + //map the property dto collection with the culture of the current variant + variant.PropertyCollectionDto = _umbracoMapper.Map( + model.PersistedContent, + context => + { + // either of these may be null and that is ok, if it's invariant they will be null which is what is expected + context.SetCulture(variant.Culture); + context.SetSegment(variant.Segment); + }); + + //now map all of the saved values to the dto + _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + } + } + + return true; + } + + protected virtual IContent GetExisting(ContentItemSave model) + { + return _contentService.GetById(model.Id); + } + + private IContent CreateNew(ContentItemSave model) + { + var contentType = _contentTypeService.Get(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No content type found with alias " + model.ContentTypeAlias); + } + return new Content( + contentType.VariesByCulture() ? null : model.Variants.First().Name, + model.ParentId, + contentType); + } + + + } +} diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs similarity index 81% rename from src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs rename to src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs index a2df34f569..ab980852e0 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs @@ -1,15 +1,14 @@ -using System.Net; -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Controllers; +using System; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Net.Http.Headers; using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.IO; -using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; +using Umbraco.Extensions; +using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.WebApi; -using ModelBindingContext = System.Web.Http.ModelBinding.ModelBindingContext; namespace Umbraco.Web.Editors.Binders { @@ -18,7 +17,8 @@ namespace Umbraco.Web.Editors.Binders /// internal class ContentModelBinderHelper { - public TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) + public TModelSave BindModelFromMultipartRequest(ActionContext actionContext, + ModelBindingContext bindingContext) where TModelSave : IHaveUploadedFiles { var result = actionContext.ReadAsMultipart(Constants.SystemDirectories.TempFileUploads); @@ -33,10 +33,11 @@ namespace Umbraco.Web.Editors.Binders var parts = file.Headers.ContentDisposition.Name.Trim('\"').Split('_'); if (parts.Length < 2) { - var response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest); - response.ReasonPhrase = "The request was not formatted correctly the file name's must be underscore delimited"; - throw new HttpResponseException(response); + bindingContext.HttpContext.SetReasonPhrase( + "The request was not formatted correctly the file name's must be underscore delimited"); + throw new HttpResponseException(HttpStatusCode.BadRequest); } + var propAlias = parts[1]; //if there are 3 parts part 3 is always culture @@ -87,7 +88,8 @@ namespace Umbraco.Web.Editors.Binders /// /// /// - public void MapPropertyValuesFromSaved(IContentProperties saveModel, ContentPropertyCollectionDto dto) + public void MapPropertyValuesFromSaved(IContentProperties saveModel, + ContentPropertyCollectionDto dto) { //NOTE: Don't convert this to linq, this is much quicker foreach (var p in saveModel.Properties) @@ -100,5 +102,7 @@ namespace Umbraco.Web.Editors.Binders } } } + + } } diff --git a/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs similarity index 57% rename from src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs rename to src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs index 1084aa16ea..86272eb12f 100644 --- a/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs @@ -1,9 +1,9 @@ using System; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core.Mapping; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Web.Composing; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Editors.Binders @@ -13,53 +13,60 @@ namespace Umbraco.Web.Editors.Binders /// internal class MediaItemBinder : IModelBinder { + private readonly IMediaService _mediaService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMediaTypeService _mediaTypeService; private readonly ContentModelBinderHelper _modelBinderHelper; - private readonly ServiceContext _services; - public MediaItemBinder() : this(Current.Services) - { - } - public MediaItemBinder(ServiceContext services) + public MediaItemBinder(IMediaService mediaService, UmbracoMapper umbracoMapper, IMediaTypeService mediaTypeService) { - _services = services; + _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _mediaTypeService = mediaTypeService ?? throw new ArgumentNullException(nameof(mediaTypeService)); + _modelBinderHelper = new ContentModelBinderHelper(); } /// /// Creates the model from the request and binds it to the context /// - /// /// /// - public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) + public Task BindModelAsync(ModelBindingContext bindingContext) { + var actionContext = bindingContext.ActionContext; var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) return false; + if (model == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); //create the dto from the persisted model if (model.PersistedContent != null) { - model.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); + model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); //now map all of the saved values to the dto _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); } model.Name = model.Name.Trim(); - return true; + bindingContext.Result = ModelBindingResult.Success(model); + return Task.CompletedTask; } private IMedia GetExisting(MediaItemSave model) { - return _services.MediaService.GetById(Convert.ToInt32(model.Id)); + return _mediaService.GetById(Convert.ToInt32(model.Id)); } private IMedia CreateNew(MediaItemSave model) { - var mediaType = _services.MediaTypeService.Get(model.ContentTypeAlias); + var mediaType = _mediaTypeService.Get(model.ContentTypeAlias); if (mediaType == null) { throw new InvalidOperationException("No media type found with alias " + model.ContentTypeAlias); diff --git a/src/Umbraco.Web/Editors/Binders/MemberBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs similarity index 76% rename from src/Umbraco.Web/Editors/Binders/MemberBinder.cs rename to src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs index c63e2c13a6..912fcc6c03 100644 --- a/src/Umbraco.Web/Editors/Binders/MemberBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Web.Models.ContentEditing; using System.Linq; -using Umbraco.Web.Composing; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core.Mapping; namespace Umbraco.Web.Editors.Binders { @@ -19,16 +19,21 @@ namespace Umbraco.Web.Editors.Binders { private readonly ContentModelBinderHelper _modelBinderHelper; private readonly IShortStringHelper _shortStringHelper; - private readonly ServiceContext _services; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; - public MemberBinder() : this(Current.Services, Current.ShortStringHelper) + public MemberBinder( + IShortStringHelper shortStringHelper, + UmbracoMapper umbracoMapper, + IMemberService memberService, + IMemberTypeService memberTypeService) { - } - public MemberBinder(ServiceContext services, IShortStringHelper shortStringHelper) - { - _services = services ?? throw new ArgumentNullException(nameof(services)); _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + _umbracoMapper = umbracoMapper ?? throw new ArgumentNullException(nameof(umbracoMapper)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); _modelBinderHelper = new ContentModelBinderHelper(); } @@ -38,24 +43,30 @@ namespace Umbraco.Web.Editors.Binders /// /// /// - public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) + public Task BindModelAsync(ModelBindingContext bindingContext) { + var actionContext = bindingContext.ActionContext; var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) return false; + if (model == null) + { + bindingContext.Result = ModelBindingResult.Failed(); + return Task.CompletedTask; + } model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); //create the dto from the persisted model if (model.PersistedContent != null) { - model.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); + model.PropertyCollectionDto = _umbracoMapper.Map(model.PersistedContent); //now map all of the saved values to the dto _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); } model.Name = model.Name.Trim(); - return true; + bindingContext.Result = ModelBindingResult.Success(model); + return Task.CompletedTask; } /// @@ -70,7 +81,7 @@ namespace Umbraco.Web.Editors.Binders private IMember GetExisting(Guid key) { - var member = _services.MemberService.GetByKey(key); + var member = _memberService.GetByKey(key); if (member == null) { throw new InvalidOperationException("Could not find member with key " + key); @@ -89,7 +100,7 @@ namespace Umbraco.Web.Editors.Binders /// private IMember CreateNew(MemberSave model) { - var contentType = _services.MemberTypeService.Get(model.ContentTypeAlias); + var contentType = _memberTypeService.Get(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No member type found with alias " + model.ContentTypeAlias); @@ -128,5 +139,6 @@ namespace Umbraco.Web.Editors.Binders } } + } } diff --git a/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs b/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs index 21bfd6f9ba..edb0749133 100644 --- a/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ActionResultExtensions.cs @@ -1,4 +1,6 @@ using System.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs index db7b553024..15d3d04c0b 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpContextExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Security.Claims; using System.Security.Principal; using System.Text; +using Microsoft.AspNetCore.Http.Features; using Umbraco.Core.BackOffice; namespace Umbraco.Extensions @@ -15,10 +16,22 @@ namespace Umbraco.Extensions context.User = principal; } + + public static void SetReasonPhrase(this HttpContext httpContext, string reasonPhrase) + { + //TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body + // https://github.com/aspnet/HttpAbstractions/issues/395 + var httpResponseFeature = httpContext.Features.Get(); + if (!(httpResponseFeature is null)) + { + httpResponseFeature.ReasonPhrase = reasonPhrase; + } + } + /// /// This will return the current back office identity. /// - /// + /// /// /// Returns the current back office identity if an admin is authenticated otherwise null /// diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 2e809bc93a..d25d9528ad 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -159,11 +159,7 @@ namespace Umbraco.Web.Editors "userGroupsApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.PostSaveUserGroup(null)) }, - { - "contentApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( - controller => controller.PostSave(null)) - }, - { + { "mediaApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetRootMedia()) }, diff --git a/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs b/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs deleted file mode 100644 index eb4d482b14..0000000000 --- a/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Editors.Binders -{ - internal class BlueprintItemBinder : ContentItemBinder - { - public BlueprintItemBinder() - { - } - - public BlueprintItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor) - : base(logger, services, umbracoContextAccessor) - { - } - - protected override IContent GetExisting(ContentItemSave model) - { - return Services.ContentService.GetBlueprintById(model.Id); - } - } -} diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs deleted file mode 100644 index a6cba6ce41..0000000000 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Composing; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Models.Mapping; - -namespace Umbraco.Web.Editors.Binders -{ - /// - /// The model binder for - /// - internal class ContentItemBinder : IModelBinder - { - private readonly ContentModelBinderHelper _modelBinderHelper; - - public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor) - { - } - - public ContentItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor) - { - Services = services; - _modelBinderHelper = new ContentModelBinderHelper(); - } - - protected ServiceContext Services { get; } - - /// - /// Creates the model from the request and binds it to the context - /// - /// - /// - /// - public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) - { - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); - if (model == null) return false; - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); - - //create the dto from the persisted model - if (model.PersistedContent != null) - { - foreach (var variant in model.Variants) - { - //map the property dto collection with the culture of the current variant - variant.PropertyCollectionDto = Current.Mapper.Map( - model.PersistedContent, - context => - { - // either of these may be null and that is ok, if it's invariant they will be null which is what is expected - context.SetCulture(variant.Culture); - context.SetSegment(variant.Segment); - }); - - //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); - } - } - - return true; - } - - protected virtual IContent GetExisting(ContentItemSave model) - { - return Services.ContentService.GetById(model.Id); - } - - private IContent CreateNew(ContentItemSave model) - { - var contentType = Services.ContentTypeService.Get(model.ContentTypeAlias); - if (contentType == null) - { - throw new InvalidOperationException("No content type found with alias " + model.ContentTypeAlias); - } - return new Content( - contentType.VariesByCulture() ? null : model.Variants.First().Name, - model.ParentId, - contentType); - } - } -} diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index b23cf57643..d9dd90f541 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -79,7 +79,7 @@ namespace Umbraco.Web.Editors // get the property editor if (propertyDto.PropertyEditor == null) { - Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); + Logger.Warn("No property editor found for property {PropertyAlias}", propertyDto.Alias); continue; } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs deleted file mode 100644 index e60e771970..0000000000 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveValidationAttribute.cs +++ /dev/null @@ -1,224 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.Actions; -using Umbraco.Web.Composing; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; -using Umbraco.Web.WebApi; - -namespace Umbraco.Web.Editors.Filters -{ - /// - /// Validates the incoming model along with if the user is allowed to perform the operation - /// - internal sealed class ContentSaveValidationAttribute : ActionFilterAttribute - { - private readonly ILogger _logger; - private readonly IWebSecurity _webSecurity; - private readonly ILocalizedTextService _textService; - private readonly IContentService _contentService; - private readonly IUserService _userService; - private readonly IEntityService _entityService; - - public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService) - { } - - public ContentSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); - _userService = userService ?? throw new ArgumentNullException(nameof(userService)); - _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - var model = (ContentItemSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService); - - if (!ValidateAtLeastOneVariantIsBeingSaved(model, actionContext)) return; - if (!contentItemValidator.ValidateExistingContent(model, actionContext)) return; - if (!ValidateUserAccess(model, actionContext, _webSecurity)) return; - - //validate for each variant that is being updated - foreach (var variant in model.Variants.Where(x => x.Save)) - { - if (contentItemValidator.ValidateProperties(model, variant, actionContext)) - contentItemValidator.ValidatePropertiesData(model, variant, variant.PropertyCollectionDto, actionContext.ModelState); - } - } - - /// - /// If there are no variants tagged for Saving, then this is an invalid request - /// - /// - /// - /// - private bool ValidateAtLeastOneVariantIsBeingSaved(ContentItemSave contentItem, HttpActionContext actionContext) - { - if (!contentItem.Variants.Any(x => x.Save)) - { - actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, "No variants flagged for saving"); - return false; - } - - return true; - } - - /// - /// Checks if the user has access to post a content item based on whether it's being created or saved. - /// - /// - /// - /// - private bool ValidateUserAccess(ContentItemSave contentItem, HttpActionContext actionContext, IWebSecurity webSecurity) - { - - // We now need to validate that the user is allowed to be doing what they are doing. - // Based on the action we need to check different permissions. - // Then if it is new, we need to lookup those permissions on the parent! - - var permissionToCheck = new List(); - IContent contentToCheck = null; - int contentIdToCheck; - switch (contentItem.Action) - { - case ContentSaveAction.Save: - permissionToCheck.Add(ActionUpdate.ActionLetter); - contentToCheck = contentItem.PersistedContent; - contentIdToCheck = contentToCheck.Id; - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishWithDescendants: - case ContentSaveAction.PublishWithDescendantsForce: - permissionToCheck.Add(ActionPublish.ActionLetter); - contentToCheck = contentItem.PersistedContent; - contentIdToCheck = contentToCheck.Id; - break; - case ContentSaveAction.SendPublish: - permissionToCheck.Add(ActionToPublish.ActionLetter); - contentToCheck = contentItem.PersistedContent; - contentIdToCheck = contentToCheck.Id; - break; - case ContentSaveAction.Schedule: - permissionToCheck.Add(ActionUpdate.ActionLetter); - permissionToCheck.Add(ActionToPublish.ActionLetter); - contentToCheck = contentItem.PersistedContent; - contentIdToCheck = contentToCheck.Id; - break; - case ContentSaveAction.SaveNew: - //Save new requires ActionNew - - permissionToCheck.Add(ActionNew.ActionLetter); - - if (contentItem.ParentId != Constants.System.Root) - { - contentToCheck = _contentService.GetById(contentItem.ParentId); - contentIdToCheck = contentToCheck.Id; - } - else - { - contentIdToCheck = contentItem.ParentId; - } - break; - case ContentSaveAction.SendPublishNew: - //Send new requires both ActionToPublish AND ActionNew - - permissionToCheck.Add(ActionNew.ActionLetter); - permissionToCheck.Add(ActionToPublish.ActionLetter); - if (contentItem.ParentId != Constants.System.Root) - { - contentToCheck = _contentService.GetById(contentItem.ParentId); - contentIdToCheck = contentToCheck.Id; - } - else - { - contentIdToCheck = contentItem.ParentId; - } - break; - case ContentSaveAction.PublishNew: - case ContentSaveAction.PublishWithDescendantsNew: - case ContentSaveAction.PublishWithDescendantsForceNew: - //Publish new requires both ActionNew AND ActionPublish - // TODO: Shouldn't publish also require ActionUpdate since it will definitely perform an update to publish but maybe that's just implied - - permissionToCheck.Add(ActionNew.ActionLetter); - permissionToCheck.Add(ActionPublish.ActionLetter); - - if (contentItem.ParentId != Constants.System.Root) - { - contentToCheck = _contentService.GetById(contentItem.ParentId); - contentIdToCheck = contentToCheck.Id; - } - else - { - contentIdToCheck = contentItem.ParentId; - } - break; - case ContentSaveAction.ScheduleNew: - - permissionToCheck.Add(ActionNew.ActionLetter); - permissionToCheck.Add(ActionUpdate.ActionLetter); - permissionToCheck.Add(ActionPublish.ActionLetter); - - if (contentItem.ParentId != Constants.System.Root) - { - contentToCheck = _contentService.GetById(contentItem.ParentId); - contentIdToCheck = contentToCheck.Id; - } - else - { - contentIdToCheck = contentItem.ParentId; - } - break; - default: - throw new ArgumentOutOfRangeException(); - } - - ContentPermissionsHelper.ContentAccess accessResult; - if (contentToCheck != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; - - accessResult = ContentPermissionsHelper.CheckPermissions( - contentToCheck, webSecurity.CurrentUser, - _userService, _entityService, permissionToCheck.ToArray()); - } - else - { - accessResult = ContentPermissionsHelper.CheckPermissions( - contentIdToCheck, webSecurity.CurrentUser, - _userService, _contentService, _entityService, - out contentToCheck, - permissionToCheck.ToArray()); - if (contentToCheck != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentToCheck; - } - } - - if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound) - throw new HttpResponseException(HttpStatusCode.NotFound); - - if (accessResult != ContentPermissionsHelper.ContentAccess.Granted) - throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); - - return accessResult == ContentPermissionsHelper.ContentAccess.Granted; - } - } -} diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 73db8bdef4..6791e480d7 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -32,7 +32,6 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Strings; using Umbraco.Web.ContentApps; -using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -201,7 +200,7 @@ namespace Umbraco.Web.Editors /// /// /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable))] + //[FilterAllowedOutgoingMedia(typeof(IEnumerable))] // TODO introduce when moved to .NET Core public IEnumerable GetByIds([FromUri]int[] ids) { var foundMedia = Services.MediaService.GetByIds(ids); @@ -245,7 +244,7 @@ namespace Umbraco.Web.Editors /// /// Returns the root media objects /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] + //[FilterAllowedOutgoingMedia(typeof(IEnumerable>))] // TODO introduce when moved to .NET Core public IEnumerable> GetRootMedia() { // TODO: Add permissions check! @@ -269,7 +268,7 @@ namespace Umbraco.Web.Editors /// /// Returns the child media objects - using the entity INT id /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core public PagedResult> GetChildren(int id, int pageNumber = 0, int pageSize = 0, @@ -346,7 +345,7 @@ namespace Umbraco.Web.Editors /// /// /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core public PagedResult> GetChildren(Guid id, int pageNumber = 0, int pageSize = 0, @@ -374,7 +373,7 @@ namespace Umbraco.Web.Editors /// /// /// - [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core public PagedResult> GetChildren(Udi id, int pageNumber = 0, int pageSize = 0, diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 9846a7aa0b..d1b833b21f 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -157,7 +157,6 @@ - @@ -224,7 +223,6 @@ - @@ -309,9 +307,7 @@ - - @@ -344,7 +340,6 @@ - @@ -396,16 +391,10 @@ - - - - - - @@ -491,5 +480,8 @@ Mvc\web.config + + + \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs deleted file mode 100644 index b1ec635269..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForContentAttribute.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Net; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Security; -using Umbraco.Web.Actions; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Auth filter to check if the current user has access to the content item (by id). - /// - /// - /// - /// This first checks if the user can access this based on their start node, and then checks node permissions - /// - /// By default the permission that is checked is browse but this can be specified in the ctor. - /// NOTE: This cannot be an auth filter because that happens too soon and we don't have access to the action params. - /// - public sealed class EnsureUserPermissionForContentAttribute : ActionFilterAttribute - { - private readonly int? _nodeId; - private readonly string _paramName; - private readonly char? _permissionToCheck; - - /// - /// This constructor will only be able to test the start node access - /// - public EnsureUserPermissionForContentAttribute(int nodeId) - { - _nodeId = nodeId; - } - - public EnsureUserPermissionForContentAttribute(int nodeId, char permissionToCheck) - : this(nodeId) - { - _permissionToCheck = permissionToCheck; - } - - public EnsureUserPermissionForContentAttribute(string paramName) - { - if (paramName == null) throw new ArgumentNullException(nameof(paramName)); - if (string.IsNullOrEmpty(paramName)) throw new ArgumentException("Value can't be empty.", nameof(paramName)); - - _paramName = paramName; - _permissionToCheck = ActionBrowse.ActionLetter; - } - - public EnsureUserPermissionForContentAttribute(string paramName, char permissionToCheck) - : this(paramName) - { - _permissionToCheck = permissionToCheck; - } - - public override bool AllowMultiple => true; - - public override void OnActionExecuting(HttpActionContext actionContext) - { - if (Current.UmbracoContext.Security.CurrentUser == null) - { - //not logged in - throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); - } - - int nodeId; - if (_nodeId.HasValue == false) - { - var parts = _paramName.Split(new[] {'.'}, StringSplitOptions.RemoveEmptyEntries); - - if (actionContext.ActionArguments[parts[0]] == null) - { - throw new InvalidOperationException("No argument found for the current action with the name: " + _paramName); - } - - if (parts.Length == 1) - { - var argument = actionContext.ActionArguments[parts[0]].ToString(); - // if the argument is an int, it will parse and can be assigned to nodeId - // if might be a udi, so check that next - // otherwise treat it as a guid - unlikely we ever get here - if (int.TryParse(argument, out int parsedId)) - { - nodeId = parsedId; - } - else if (UdiParser.TryParse(argument, true, out Udi udi)) - { - // TODO: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services - nodeId = Current.Services.EntityService.GetId(udi).Result; - } - else - { - Guid.TryParse(argument, out Guid key); - // TODO: inject? we can't because this is an attribute but we could provide ctors and empty ctors that pass in the required services - nodeId = Current.Services.EntityService.GetId(key, UmbracoObjectTypes.Document).Result; - } - } - else - { - //now we need to see if we can get the property of whatever object it is - var pType = actionContext.ActionArguments[parts[0]].GetType(); - var prop = pType.GetProperty(parts[1]); - if (prop == null) - { - throw new InvalidOperationException("No argument found for the current action with the name: " + _paramName); - } - nodeId = (int)prop.GetValue(actionContext.ActionArguments[parts[0]]); - } - } - else - { - nodeId = _nodeId.Value; - } - - var permissionResult = ContentPermissionsHelper.CheckPermissions(nodeId, - Current.UmbracoContext.Security.CurrentUser, - Current.Services.UserService, - Current.Services.ContentService, - Current.Services.EntityService, - out var contentItem, - _permissionToCheck.HasValue ? new[] { _permissionToCheck.Value } : null); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.NotFound) - throw new HttpResponseException(HttpStatusCode.NotFound); - - if (permissionResult == ContentPermissionsHelper.ContentAccess.Denied) - throw new HttpResponseException(actionContext.Request.CreateUserNoAccessResponse()); - - if (contentItem != null) - { - //store the content item in request cache so it can be resolved in the controller without re-looking it up - actionContext.Request.Properties[typeof(IContent).ToString()] = contentItem; - } - - base.OnActionExecuting(actionContext); - } - } -}