diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs similarity index 78% rename from src/Umbraco.Web/Editors/MediaController.cs rename to src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index 6791e480d7..0a44d360d9 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -4,12 +4,9 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Formatting; using System.Text; using System.Threading.Tasks; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.ModelBinding; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -17,12 +14,9 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Entities; @@ -30,13 +24,18 @@ using Umbraco.Core.Models.Validation; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Strings; using Umbraco.Web.ContentApps; -using Umbraco.Web.Editors.Filters; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Mapping; -using Umbraco.Web.Routing; +using Umbraco.Core.Strings; +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.Editors.Binders; +using Umbraco.Web.Security; namespace Umbraco.Web.Editors { @@ -46,47 +45,55 @@ namespace Umbraco.Web.Editors /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorize(Constants.Applications.Media)] - [MediaControllerControllerConfiguration] public class MediaController : ContentControllerBase { private readonly IContentSettings _contentSettings; private readonly IIOHelper _ioHelper; - + private readonly IMediaTypeService _mediaTypeService; + private readonly IMediaService _mediaService; + private readonly IEntityService _entityService; + private readonly IWebSecurity _webSecurity; + private readonly UmbracoMapper _umbracoMapper; + private readonly IDataTypeService _dataTypeService; + private readonly ILocalizedTextService _localizedTextService; + private readonly ISqlContext _sqlContext; + private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider; + private readonly IRelationService _relationService; public MediaController( ICultureDictionary cultureDictionary, - PropertyEditorCollection propertyEditors, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IMediaFileSystem mediaFileSystem, + ILogger logger, IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, + IEventMessagesFactory eventMessages, + ILocalizedTextService localizedTextService, IContentSettings contentSettings, IIOHelper ioHelper, - IPublishedUrlProvider publishedUrlProvider) - : base(cultureDictionary, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) + IMediaTypeService mediaTypeService, + IMediaService mediaService, + IEntityService entityService, + IWebSecurity webSecurity, + UmbracoMapper umbracoMapper, + IDataTypeService dataTypeService, + ISqlContext sqlContext, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IRelationService relationService, + PropertyEditorCollection propertyEditors, + IMediaFileSystem mediaFileSystem) + : base(cultureDictionary, logger, shortStringHelper, eventMessages, localizedTextService) { - _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); + _contentSettings = contentSettings; + _ioHelper = ioHelper; + _mediaTypeService = mediaTypeService; + _mediaService = mediaService; + _entityService = entityService; + _webSecurity = webSecurity; + _umbracoMapper = umbracoMapper; + _dataTypeService = dataTypeService; + _localizedTextService = localizedTextService; + _sqlContext = sqlContext; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _relationService = relationService; + _propertyEditors = propertyEditors; _mediaFileSystem = mediaFileSystem; - _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); - _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - } - - /// - /// Configures this controller with a custom action selector - /// - private class MediaControllerControllerConfigurationAttribute : Attribute, IControllerConfiguration - { - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - controllerSettings.Services.Replace(typeof(IHttpActionSelector), new ParameterSwapControllerActionSelector( - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), - new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetChildren", "id", typeof(int), typeof(Guid), typeof(Udi)))); - } } /// @@ -98,14 +105,14 @@ namespace Umbraco.Web.Editors // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) { - var contentType = Services.MediaTypeService.Get(contentTypeAlias); + var contentType = _mediaTypeService.Get(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); - var mapped = Mapper.Map(emptyContent); + var emptyContent = _mediaService.CreateMedia("", parentId, contentType.Alias, _webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); + var mapped = _umbracoMapper.Map(emptyContent); //remove the listview app if it exists mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); @@ -120,14 +127,14 @@ namespace Umbraco.Web.Editors public MediaItemDisplay GetRecycleBin() { var apps = new List(); - apps.Add(ListViewContentAppFactory.CreateContentApp(Services.DataTypeService, _propertyEditors, "recycleBin", "media", Core.Constants.DataTypes.DefaultMediaListView)); + apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, "recycleBin", "media", Core.Constants.DataTypes.DefaultMediaListView)); apps[0].Active = true; var display = new MediaItemDisplay { Id = Constants.System.RecycleBinMedia, Alias = "recycleBin", ParentId = -1, - Name = Services.TextService.Localize("general/recycleBin"), + Name = _localizedTextService.Localize("general/recycleBin"), ContentTypeAlias = "recycleBin", CreateDate = DateTime.Now, IsContainer = true, @@ -145,9 +152,10 @@ namespace Umbraco.Web.Editors /// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core [EnsureUserPermissionForMedia("id")] + [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(int id) { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _mediaService.GetById(id)); if (foundContent == null) { @@ -155,7 +163,7 @@ namespace Umbraco.Web.Editors //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return _umbracoMapper.Map(foundContent); } /// @@ -165,9 +173,10 @@ namespace Umbraco.Web.Editors /// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core [EnsureUserPermissionForMedia("id")] + [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Guid id) { - var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + var foundContent = GetObjectFromRequest(() => _mediaService.GetById(id)); if (foundContent == null) { @@ -175,7 +184,7 @@ namespace Umbraco.Web.Editors //HandleContentNotFound will throw an exception return null; } - return Mapper.Map(foundContent); + return _umbracoMapper.Map(foundContent); } /// @@ -185,6 +194,7 @@ namespace Umbraco.Web.Editors /// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core [EnsureUserPermissionForMedia("id")] + [DetermineAmbiguousActionByPassingParameters] public MediaItemDisplay GetById(Udi id) { var guidUdi = id as GuidUdi; @@ -201,10 +211,10 @@ namespace Umbraco.Web.Editors /// /// //[FilterAllowedOutgoingMedia(typeof(IEnumerable))] // TODO introduce when moved to .NET Core - public IEnumerable GetByIds([FromUri]int[] ids) + public IEnumerable GetByIds([FromQuery]int[] ids) { - var foundMedia = Services.MediaService.GetByIds(ids); - return foundMedia.Select(media => Mapper.Map(media)); + var foundMedia = _mediaService.GetByIds(ids); + return foundMedia.Select(media => _umbracoMapper.Map(media)); } /// @@ -218,7 +228,7 @@ namespace Umbraco.Web.Editors { //Suggested convention for folder mediatypes - we can make this more or less complicated as long as we document it... //if you create a media type, which has an alias that ends with ...Folder then its a folder: ex: "secureFolder", "bannerFolder", "Folder" - var folderTypes = Services.MediaTypeService + var folderTypes = _mediaTypeService .GetAll() .Where(x => x.Alias.EndsWith("Folder")) .Select(x => x.Id) @@ -230,14 +240,14 @@ namespace Umbraco.Web.Editors } long total; - var children = Services.MediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out total, + var children = _mediaService.GetPagedChildren(id, pageNumber - 1, pageSize, out total, //lookup these content types - SqlContext.Query().Where(x => folderTypes.Contains(x.ContentTypeId)), + _sqlContext.Query().Where(x => folderTypes.Contains(x.ContentTypeId)), Ordering.By("Name", Direction.Ascending)); return new PagedResult>(total, pageNumber, pageSize) { - Items = children.Select(Mapper.Map>) + Items = children.Select(_umbracoMapper.Map>) }; } @@ -249,8 +259,8 @@ namespace Umbraco.Web.Editors { // TODO: Add permissions check! - return Services.MediaService.GetRootMedia() - .Select(Mapper.Map>); + return _mediaService.GetRootMedia() + .Select(_umbracoMapper.Map>); } #region GetChildren @@ -262,13 +272,14 @@ namespace Umbraco.Web.Editors protected int[] UserStartNodes { - get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService)); } + get { return _userStartNodes ?? (_userStartNodes = _webSecurity.CurrentUser.CalculateMediaStartNodeIds(_entityService)); } } /// /// Returns the child media objects - using the entity INT id /// - //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core//[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + [DetermineAmbiguousActionByPassingParameters] public PagedResult> GetChildren(int id, int pageNumber = 0, int pageSize = 0, @@ -283,13 +294,13 @@ namespace Umbraco.Web.Editors { if (pageNumber > 0) return new PagedResult>(0, 0, 0); - var nodes = Services.MediaService.GetByIds(UserStartNodes).ToArray(); + var nodes = _mediaService.GetByIds(UserStartNodes).ToArray(); if (nodes.Length == 0) return new PagedResult>(0, 0, 0); if (pageSize < nodes.Length) pageSize = nodes.Length; // bah var pr = new PagedResult>(nodes.Length, pageNumber, pageSize) { - Items = nodes.Select(Mapper.Map>) + Items = nodes.Select(_umbracoMapper.Map>) }; return pr; } @@ -304,11 +315,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.MediaService + children = _mediaService .GetPagedChildren( id, (pageNumber - 1), pageSize, out totalChildren, @@ -318,7 +329,7 @@ namespace Umbraco.Web.Editors else { //better to not use this without paging where possible, currently only the sort dialog does - children = Services.MediaService.GetPagedChildren(id,0, int.MaxValue, out var total).ToList(); + children = _mediaService.GetPagedChildren(id,0, int.MaxValue, out var total).ToList(); totalChildren = children.Count; } @@ -329,7 +340,7 @@ namespace Umbraco.Web.Editors var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); pagedResult.Items = children - .Select(Mapper.Map>); + .Select(_umbracoMapper.Map>); return pagedResult; } @@ -345,7 +356,8 @@ namespace Umbraco.Web.Editors /// /// /// - //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + [DetermineAmbiguousActionByPassingParameters] public PagedResult> GetChildren(Guid id, int pageNumber = 0, int pageSize = 0, @@ -354,7 +366,7 @@ namespace Umbraco.Web.Editors bool orderBySystemField = true, string filter = "") { - var entity = Services.EntityService.Get(id); + var entity = _entityService.Get(id); if (entity != null) { return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); @@ -373,7 +385,8 @@ namespace Umbraco.Web.Editors /// /// /// - //[FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] // TODO introduce when moved to .NET Core + [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] + [DetermineAmbiguousActionByPassingParameters] public PagedResult> GetChildren(Udi id, int pageNumber = 0, int pageSize = 0, @@ -385,7 +398,7 @@ namespace Umbraco.Web.Editors var guidUdi = id as GuidUdi; if (guidUdi != null) { - var entity = Services.EntityService.Get(guidUdi.Guid); + var entity = _entityService.Get(guidUdi.Guid); if (entity != null) { return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); @@ -406,7 +419,7 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteById(int id) { - var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); + var foundMedia = GetObjectFromRequest(() => _mediaService.GetById(id)); if (foundMedia == null) { @@ -416,7 +429,7 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundMedia.Trashed == false) { - var moveResult = Services.MediaService.MoveToRecycleBin(foundMedia, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + var moveResult = _mediaService.MoveToRecycleBin(foundMedia, _webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -426,7 +439,7 @@ namespace Umbraco.Web.Editors } else { - var deleteResult = Services.MediaService.Delete(foundMedia, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + var deleteResult = _mediaService.Delete(foundMedia, _webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -450,11 +463,11 @@ namespace Umbraco.Web.Editors var destinationParentID = move.ParentId; var sourceParentID = toMove.ParentId; - var moveResult = Services.MediaService.Move(toMove, move.ParentId, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + var moveResult = _mediaService.Move(toMove, move.ParentId, _webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); if (sourceParentID == destinationParentID) { - return Request.CreateValidationErrorResponse(new SimpleNotificationModel(new BackOfficeNotification("",Services.TextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); + return Request.CreateValidationErrorResponse(new SimpleNotificationModel(new BackOfficeNotification("",_localizedTextService.Localize("media/moveToSameFolderFailed"),NotificationStyle.Error))); } if (moveResult == false) { @@ -519,17 +532,17 @@ namespace Umbraco.Web.Editors { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the model state to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); + var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } } //save the item - var saveStatus = Services.MediaService.Save(contentItem.PersistedContent, Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + var saveStatus = _mediaService.Save(contentItem.PersistedContent, _webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); + var display = _umbracoMapper.Map(contentItem.PersistedContent); //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -542,8 +555,8 @@ namespace Umbraco.Web.Editors if (saveStatus.Success) { display.AddSuccessNotification( - Services.TextService.Localize("speechBubbles/editMediaSaved"), - Services.TextService.Localize("speechBubbles/editMediaSavedText")); + _localizedTextService.Localize("speechBubbles/editMediaSaved"), + _localizedTextService.Localize("speechBubbles/editMediaSavedText")); } else { @@ -572,9 +585,9 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage EmptyRecycleBin() { - Services.MediaService.EmptyRecycleBin(Security.GetUserId().ResultOr(Constants.Security.SuperUserId)); + _mediaService.EmptyRecycleBin(_webSecurity.GetUserId().ResultOr(Constants.Security.SuperUserId)); - return Request.CreateNotificationSuccessResponse(Services.TextService.Localize("defaultdialogs/recycleBinIsEmpty")); + return Request.CreateNotificationSuccessResponse(_localizedTextService.Localize("defaultdialogs/recycleBinIsEmpty")); } /// @@ -596,7 +609,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - var mediaService = Services.MediaService; + var mediaService = _mediaService; var sortedMedia = new List(); try { @@ -621,12 +634,12 @@ namespace Umbraco.Web.Editors { var intParentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true); - var mediaService = Services.MediaService; + var mediaService = _mediaService; var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder); - mediaService.Save(f, Security.CurrentUser.Id); + mediaService.Save(f, _webSecurity.CurrentUser.Id); - return Mapper.Map(f); + return _umbracoMapper.Map(f); } /// @@ -662,7 +675,7 @@ namespace Umbraco.Web.Editors int parentId = GetParentIdAsInt(currentFolderId, validatePermissions: true); var tempFiles = new PostedFiles(); - var mediaService = Services.MediaService; + var mediaService = _mediaService; //in case we pass a path with a folder in it, we will create it and upload media to it. if (result.FormData.ContainsKey("path")) @@ -739,21 +752,21 @@ namespace Umbraco.Web.Editors var mediaItemName = fileName.ToFriendlyName(); - var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id); + var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, _webSecurity.CurrentUser.Id); var fileInfo = new FileInfo(file.LocalFileName); var fs = fileInfo.OpenReadWithRetry(); if (fs == null) throw new InvalidOperationException("Could not acquire file stream"); using (fs) { - f.SetValue(_mediaFileSystem, ShortStringHelper, Services.ContentTypeBaseServices, Constants.Conventions.Media.File,fileName, fs); + f.SetValue(_mediaFileSystem, ShortStringHelper, _contentTypeBaseServiceProvider, Constants.Conventions.Media.File,fileName, fs); } - var saveResult = mediaService.Save(f, Security.CurrentUser.Id); + var saveResult = mediaService.Save(f, _webSecurity.CurrentUser.Id); if (saveResult == false) { AddCancelMessage(tempFiles, - message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName); + message: _localizedTextService.Localize("speechBubbles/operationCancelledText") + " -- " + mediaItemName); } else { @@ -768,8 +781,8 @@ namespace Umbraco.Web.Editors else { tempFiles.Notifications.Add(new BackOfficeNotification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("media/disallowedFileType"), + _localizedTextService.Localize("speechBubbles/operationFailedHeader"), + _localizedTextService.Localize("media/disallowedFileType"), NotificationStyle.Warning)); } } @@ -797,8 +810,8 @@ namespace Umbraco.Web.Editors var total = long.MaxValue; while (page * pageSize < total) { - var children = Services.MediaService.GetPagedChildren(mediaId, page, pageSize, out total, - SqlContext.Query().Where(x => x.Name == nameToFind)); + var children = _mediaService.GetPagedChildren(mediaId, page, pageSize, out total, + _sqlContext.Query().Where(x => x.Name == nameToFind)); foreach (var c in children) return c; //return first one if any are found } @@ -814,7 +827,7 @@ namespace Umbraco.Web.Editors /// and if that check fails an unauthorized exception will occur /// /// - private int GetParentIdAsInt(string parentId, bool validatePermissions) + private ActionResult GetParentIdAsInt(string parentId, bool validatePermissions) { int intParentId; @@ -831,37 +844,36 @@ namespace Umbraco.Web.Editors Guid idGuid; if (Guid.TryParse(parentId, out idGuid)) { - var entity = Services.EntityService.Get(idGuid); + var entity = _entityService.Get(idGuid); if (entity != null) { intParentId = entity.Id; } else { - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.NotFound, "The passed id doesn't exist")); + return NotFound("The passed id doesn't exist"); } } else { - throw new HttpResponseException( - Request.CreateValidationErrorResponse("The request was not formatted correctly, the parentId is not an integer, Guid or UDI")); + throw HttpResponseException.CreateValidationErrorResponse("The request was not formatted correctly, the parentId is not an integer, Guid or UDI"); } } //ensure the user has access to this folder by parent id! if (validatePermissions && CheckPermissions( - new Dictionary(), - Security.CurrentUser, - Services.MediaService, - Services.EntityService, + new Dictionary(), + _webSecurity.CurrentUser, + _mediaService, + _entityService, intParentId) == false) { - throw new HttpResponseException(Request.CreateResponse( + throw new HttpResponseException( HttpStatusCode.Forbidden, new SimpleNotificationModel(new BackOfficeNotification( - Services.TextService.Localize("speechBubbles/operationFailedHeader"), - Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), - NotificationStyle.Warning)))); + _localizedTextService.Localize("speechBubbles/operationFailedHeader"), + _localizedTextService.Localize("speechBubbles/invalidUserPermissionsText"), + NotificationStyle.Warning))); } return intParentId; @@ -879,7 +891,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var mediaService = Services.MediaService; + var mediaService = _mediaService; var toMove = mediaService.GetById(model.Id); if (toMove == null) { @@ -889,12 +901,12 @@ namespace Umbraco.Web.Editors { //cannot move if the content item is not allowed at the root unless there are //none allowed at root (in which case all should be allowed at root) - var mediaTypeService = Services.MediaTypeService; + var mediaTypeService = _mediaTypeService; if (toMove.ContentType.AllowedAsRoot == false && mediaTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedAtRoot"), ""); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } } else @@ -906,21 +918,21 @@ namespace Umbraco.Web.Editors } //check if the item is allowed under this one - var parentContentType = Services.MediaTypeService.Get(parent.ContentTypeId); + var parentContentType = _mediaTypeService.Get(parent.ContentTypeId); if (parentContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByContentType"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByContentType"), ""); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { var notificationModel = new SimpleNotificationModel(); - notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); - throw new HttpResponseException(Request.CreateValidationErrorResponse(notificationModel)); + notificationModel.AddErrorNotification(_localizedTextService.Localize("moveOrCopy/notAllowedByPath"), ""); + throw HttpResponseException.CreateValidationErrorResponse(notificationModel); } } @@ -938,7 +950,7 @@ namespace Umbraco.Web.Editors /// The content to lookup, if the contentItem is not specified /// Specifies the already resolved content item to check against, setting this ignores the nodeId /// - internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null) + internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, IEntityService entityService, int nodeId, IMedia media = null) { if (storage == null) throw new ArgumentNullException("storage"); if (user == null) throw new ArgumentNullException("user"); @@ -977,7 +989,7 @@ namespace Umbraco.Web.Editors var objectType = ObjectTypes.GetUmbracoObjectType(entityType); var udiType = ObjectTypes.GetUdiType(objectType); - var relations = Services.RelationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out var totalRecords, objectType); + var relations = _relationService.GetPagedParentEntitiesByChildId(id, pageNumber - 1, pageSize, out var totalRecords, objectType); return new PagedResult(totalRecords, pageNumber, pageSize) { diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs new file mode 100644 index 0000000000..5048a3251f --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -0,0 +1,476 @@ +// using System; +// using System.Collections.Generic; +// using System.ComponentModel.DataAnnotations; +// using System.Linq; +// using System.Net; +// using System.Net.Http; +// using System.Net.Http.Formatting; +// using System.Net.Http.Headers; +// using System.Threading.Tasks; +// using System.Web.Http; +// using System.Web.Http.ModelBinding; +// using Microsoft.AspNetCore.Identity; +// using Microsoft.AspNetCore.Mvc; +// using Umbraco.Core; +// using Umbraco.Core.Cache; +// using Umbraco.Core.Configuration; +// using Umbraco.Core.Dictionary; +// using Umbraco.Core.Logging; +// using Umbraco.Core.Models; +// using Umbraco.Core.Models.ContentEditing; +// using Umbraco.Core.Models.Membership; +// using Umbraco.Core.Persistence; +// using Umbraco.Core.PropertyEditors; +// using Umbraco.Core.Security; +// using Umbraco.Core.Services; +// using Umbraco.Core.Services.Implement; +// using Umbraco.Core.Strings; +// using Umbraco.Web.ContentApps; +// using Umbraco.Web.Editors.Binders; +// using Umbraco.Web.Editors.Filters; +// using Umbraco.Web.Models.ContentEditing; +// using Umbraco.Web.Mvc; +// using Umbraco.Web.WebApi; +// using Umbraco.Web.WebApi.Filters; +// using Constants = Umbraco.Core.Constants; +// using Umbraco.Core.Mapping; +// using Umbraco.Extensions; +// using Umbraco.Web.BackOffice.Filters; +// using Umbraco.Web.Common.Attributes; +// using Umbraco.Web.Common.Exceptions; +// using Umbraco.Web.Routing; +// +// namespace Umbraco.Web.Editors +// { +// /// +// /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting +// /// access to ALL of the methods on this controller will need access to the member application. +// /// +// [PluginController("UmbracoApi")] +// [UmbracoApplicationAuthorize(Constants.Applications.Members)] +// [OutgoingNoHyphenGuidFormat] +// public class MemberController : ContentControllerBase +// { +// public MemberController( +// IMemberPasswordConfiguration passwordConfig, +// ICultureDictionary cultureDictionary, +// PropertyEditorCollection propertyEditors, +// IGlobalSettings globalSettings, +// IUmbracoContextAccessor umbracoContextAccessor, +// ISqlContext sqlContext, +// ServiceContext services, +// AppCaches appCaches, +// IProfilingLogger logger, +// IRuntimeState runtimeState, +// IShortStringHelper shortStringHelper, +// UmbracoMapper umbracoMapper, +// IPublishedUrlProvider publishedUrlProvider) +// : base(cultureDictionary, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) +// { +// _passwordConfig = passwordConfig ?? throw new ArgumentNullException(nameof(passwordConfig)); +// _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); +// _passwordSecurity = new LegacyPasswordSecurity(_passwordConfig); +// _passwordValidator = new ConfiguredPasswordValidator(); +// } +// +// private readonly IMemberPasswordConfiguration _passwordConfig; +// private readonly PropertyEditorCollection _propertyEditors; +// private readonly LegacyPasswordSecurity _passwordSecurity; +// private readonly IPasswordValidator<> _passwordValidator; +// +// public PagedResult GetPagedResults( +// int pageNumber = 1, +// int pageSize = 100, +// string orderBy = "username", +// Direction orderDirection = Direction.Ascending, +// bool orderBySystemField = true, +// string filter = "", +// string memberTypeAlias = null) +// { +// +// if (pageNumber <= 0 || pageSize <= 0) +// { +// throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); +// } +// +// var members = Services.MemberService +// .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); +// if (totalRecords == 0) +// { +// return new PagedResult(0, 0, 0); +// } +// +// var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) +// { +// Items = members +// .Select(x => Mapper.Map(x)) +// }; +// return pagedResult; +// } +// +// /// +// /// Returns a display node with a list view to render members +// /// +// /// +// /// +// public MemberListDisplay GetListNodeDisplay(string listName) +// { +// var foundType = Services.MemberTypeService.Get(listName); +// var name = foundType != null ? foundType.Name : listName; +// +// var apps = new List(); +// apps.Add(ListViewContentAppFactory.CreateContentApp(Services.DataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); +// apps[0].Active = true; +// +// var display = new MemberListDisplay +// { +// ContentTypeAlias = listName, +// ContentTypeName = name, +// Id = listName, +// IsContainer = true, +// Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, +// Path = "-1," + listName, +// ParentId = -1, +// ContentApps = apps +// }; +// +// return display; +// } +// +// /// +// /// Gets the content json for the member +// /// +// /// +// /// +// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core +// public MemberDisplay GetByKey(Guid key) +// { +// var foundMember = Services.MemberService.GetByKey(key); +// if (foundMember == null) +// { +// HandleContentNotFound(key); +// } +// return Mapper.Map(foundMember); +// } +// +// /// +// /// Gets an empty content item for the +// /// +// /// +// /// +// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core +// public MemberDisplay GetEmpty(string contentTypeAlias = null) +// { +// IMember emptyContent; +// if (contentTypeAlias == null) +// { +// throw new HttpResponseException(HttpStatusCode.NotFound); +// } +// +// var contentType = Services.MemberTypeService.Get(contentTypeAlias); +// if (contentType == null) +// { +// throw new HttpResponseException(HttpStatusCode.NotFound); +// } +// +// var passwordGenerator = new PasswordGenerator(_passwordConfig); +// +// emptyContent = new Member(contentType); +// emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); +// return Mapper.Map(emptyContent); +// } +// +// /// +// /// Saves member +// /// +// /// +// [FileUploadCleanupFilter] +// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core +// [MemberSaveValidation] +// public async Task PostSave( +// [ModelBinder(typeof(MemberBinder))] +// MemberSave contentItem) +// { +// +// //If we've reached here it means: +// // * Our model has been bound +// // * and validated +// // * any file attachments have been saved to their temporary location for us to use +// // * we have a reference to the DTO object and the persisted object +// // * Permissions are valid +// +// //map the properties to the persisted entity +// MapPropertyValues(contentItem); +// +// await ValidateMemberDataAsync(contentItem); +// +// //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors +// if (ModelState.IsValid == false) +// { +// var forDisplay = Mapper.Map(contentItem.PersistedContent); +// forDisplay.Errors = ModelState.ToErrorDictionary(); +// throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); +// } +// +// //We're gonna look up the current roles now because the below code can cause +// // events to be raised and developers could be manually adding roles to members in +// // their handlers. If we don't look this up now there's a chance we'll just end up +// // removing the roles they've assigned. +// var currRoles = Services.MemberService.GetAllRoles(contentItem.PersistedContent.Username); +// //find the ones to remove and remove them +// var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); +// +// //Depending on the action we need to first do a create or update using the membership provider +// // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. +// switch (contentItem.Action) +// { +// case ContentSaveAction.Save: +// UpdateMemberData(contentItem); +// break; +// case ContentSaveAction.SaveNew: +// contentItem.PersistedContent = CreateMemberData(contentItem); +// break; +// default: +// //we don't support anything else for members +// throw new HttpResponseException(HttpStatusCode.NotFound); +// } +// +// //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope +// // but it would be nicer to have this taken care of within the Save method itself +// +// //create/save the IMember +// Services.MemberService.Save(contentItem.PersistedContent); +// +// //Now let's do the role provider stuff - now that we've saved the content item (that is important since +// // if we are changing the username, it must be persisted before looking up the member roles). +// if (rolesToRemove.Any()) +// { +// Services.MemberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); +// } +// //find the ones to add and add them +// var toAdd = contentItem.Groups.Except(currRoles).ToArray(); +// if (toAdd.Any()) +// { +// //add the ones submitted +// Services.MemberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); +// } +// +// //return the updated model +// var display = Mapper.Map(contentItem.PersistedContent); +// +// //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 +// HandleInvalidModelState(display); +// +// var localizedTextService = Services.TextService; +// //put the correct messages in +// switch (contentItem.Action) +// { +// case ContentSaveAction.Save: +// case ContentSaveAction.SaveNew: +// display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); +// break; +// } +// +// return display; +// } +// +// /// +// /// Maps the property values to the persisted entity +// /// +// /// +// private void MapPropertyValues(MemberSave contentItem) +// { +// UpdateName(contentItem); +// +// //map the custom properties - this will already be set for new entities in our member binder +// contentItem.PersistedContent.Email = contentItem.Email; +// contentItem.PersistedContent.Username = contentItem.Username; +// +// //use the base method to map the rest of the properties +// base.MapPropertyValuesForPersistence( +// contentItem, +// contentItem.PropertyCollectionDto, +// (save, property) => property.GetValue(), //get prop val +// (save, property, v) => property.SetValue(v), //set prop val +// null); // member are all invariant +// } +// +// private IMember CreateMemberData(MemberSave contentItem) +// { +// var memberType = Services.MemberTypeService.Get(contentItem.ContentTypeAlias); +// if (memberType == null) +// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); +// var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) +// { +// CreatorId = Security.CurrentUser.Id, +// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), +// Comments = contentItem.Comments, +// IsApproved = contentItem.IsApproved +// }; +// +// return member; +// } +// +// /// +// /// Update the member security data +// /// +// /// +// /// +// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. +// /// +// private void UpdateMemberData(MemberSave contentItem) +// { +// contentItem.PersistedContent.WriterId = Security.CurrentUser.Id; +// +// // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types +// // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. +// // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut +// // but we will take care of this in a generic way below so that it works for all props. +// if (!Security.CurrentUser.HasAccessToSensitiveData()) +// { +// var memberType = Services.MemberTypeService.Get(contentItem.PersistedContent.ContentTypeId); +// var sensitiveProperties = memberType +// .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) +// .ToList(); +// +// foreach (var sensitiveProperty in sensitiveProperties) +// { +// var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); +// if (destProp != null) +// { +// //if found, change the value of the contentItem model to the persisted value so it remains unchanged +// var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); +// destProp.Value = origValue; +// } +// } +// } +// +// var isLockedOut = contentItem.IsLockedOut; +// +// //if they were locked but now they are trying to be unlocked +// if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) +// { +// contentItem.PersistedContent.IsLockedOut = false; +// contentItem.PersistedContent.FailedPasswordAttempts = 0; +// } +// else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut) +// { +// //NOTE: This should not ever happen unless someone is mucking around with the request data. +// //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them +// ModelState.AddModelError("custom", "An admin cannot lock a user"); +// } +// +// //no password changes then exit ? +// if (contentItem.Password == null) +// return; +// +// // set the password +// contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword); +// } +// +// private static void UpdateName(MemberSave memberSave) +// { +// //Don't update the name if it is empty +// if (memberSave.Name.IsNullOrWhiteSpace() == false) +// { +// memberSave.PersistedContent.Name = memberSave.Name; +// } +// } +// +// // TODO: This logic should be pulled into the service layer +// private async Task ValidateMemberDataAsync(MemberSave contentItem) +// { +// if (contentItem.Name.IsNullOrWhiteSpace()) +// { +// ModelState.AddPropertyError( +// new ValidationResult("Invalid user name", new[] { "value" }), +// string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); +// return false; +// } +// +// if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) +// { +// var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword); +// if (!validPassword) +// { +// ModelState.AddPropertyError( +// new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }), +// string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); +// return false; +// } +// } +// +// var byUsername = Services.MemberService.GetByUsername(contentItem.Username); +// if (byUsername != null && byUsername.Key != contentItem.Key) +// { +// ModelState.AddPropertyError( +// new ValidationResult("Username is already in use", new[] { "value" }), +// string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); +// return false; +// } +// +// var byEmail = Services.MemberService.GetByEmail(contentItem.Email); +// if (byEmail != null && byEmail.Key != contentItem.Key) +// { +// ModelState.AddPropertyError( +// new ValidationResult("Email address is already in use", new[] { "value" }), +// string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); +// return false; +// } +// +// return true; +// } +// +// /// +// /// Permanently deletes a member +// /// +// /// +// /// +// /// +// [HttpPost] +// public HttpResponseMessage DeleteByKey(Guid key) +// { +// var foundMember = Services.MemberService.GetByKey(key); +// if (foundMember == null) +// { +// return HandleContentNotFound(key, false); +// } +// Services.MemberService.Delete(foundMember); +// +// return Request.CreateResponse(HttpStatusCode.OK); +// } +// +// /// +// /// Exports member data based on their unique Id +// /// +// /// The unique member identifier +// /// +// [HttpGet] +// public HttpResponseMessage ExportMemberData(Guid key) +// { +// var currentUser = Security.CurrentUser; +// +// var httpResponseMessage = Request.CreateResponse(); +// if (currentUser.HasAccessToSensitiveData() == false) +// { +// httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; +// return httpResponseMessage; +// } +// +// var member = ((MemberService)Services.MemberService).ExportMember(key); +// +// var fileName = $"{member.Name}_{member.Email}.txt"; +// +// httpResponseMessage.Content = new ObjectContent(member, new JsonMediaTypeFormatter { Indent = true }); +// httpResponseMessage.Content.Headers.Add("x-filename", fileName); +// httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); +// httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); +// httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; +// httpResponseMessage.StatusCode = HttpStatusCode.OK; +// +// return httpResponseMessage; +// } +// } +// +// +// } diff --git a/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs new file mode 100644 index 0000000000..b75b684f5c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/EnsureUserPermissionForMediaAttribute.cs @@ -0,0 +1,180 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Editors; +using Umbraco.Web.Security; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Auth filter to check if the current user has access to the content item + /// + /// + /// Since media doesn't have permissions, this simply checks start node access + /// + internal sealed class EnsureUserPermissionForMediaAttribute : TypeFilterAttribute + { + public EnsureUserPermissionForMediaAttribute(int nodeId) + : base(typeof(EnsureUserPermissionForMediaFilter)) + { + Arguments = new object[] + { + nodeId + }; + } + + public EnsureUserPermissionForMediaAttribute(string paramName) + : base(typeof(EnsureUserPermissionForMediaFilter)) + { + Arguments = new object[] + { + paramName + }; + } + private sealed class EnsureUserPermissionForMediaFilter : IActionFilter + { + private readonly int? _nodeId; + private readonly string _paramName; + private readonly IWebSecurity _webSecurity; + private readonly IEntityService _entityService; + private readonly IMediaService _mediaService; + + /// + /// This constructor will only be able to test the start node access + /// + public EnsureUserPermissionForMediaFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IMediaService mediaService, + int nodeId) + :this(webSecurity, entityService, mediaService, nodeId, null) + { + _nodeId = nodeId; + } + + public EnsureUserPermissionForMediaFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IMediaService mediaService, + string paramName) + :this(webSecurity, entityService, mediaService,null, paramName) + { + if (paramName == null) throw new ArgumentNullException(nameof(paramName)); + if (string.IsNullOrEmpty(paramName)) + throw new ArgumentException("Value can't be empty.", nameof(paramName)); + } + + private EnsureUserPermissionForMediaFilter( + IWebSecurity webSecurity, + IEntityService entityService, + IMediaService mediaService, + int? nodeId, string paramName) + { + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _entityService = entityService ?? throw new ArgumentNullException(nameof(entityService)); + _mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService)); + + _paramName = paramName; + + if (nodeId.HasValue) + { + _nodeId = nodeId.Value; + } + } + + private int GetNodeIdFromParameter(object parameterValue) + { + if (parameterValue is int) + { + return (int) parameterValue; + } + + var guidId = Guid.Empty; + if (parameterValue is Guid) + { + guidId = (Guid) parameterValue; + } + else if (parameterValue is GuidUdi) + { + guidId = ((GuidUdi) parameterValue).Guid; + } + + if (guidId != Guid.Empty) + { + var found = _entityService.GetId(guidId, UmbracoObjectTypes.Media); + if (found) + return found.Result; + } + + throw new InvalidOperationException("The id type: " + parameterValue.GetType() + + " is not a supported id"); + } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (_webSecurity.CurrentUser == null) + { + throw new HttpResponseException(System.Net.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) + { + nodeId = GetNodeIdFromParameter(context.ActionArguments[parts[0]]); + } + 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 = GetNodeIdFromParameter(prop.GetValue(context.ActionArguments[parts[0]])); + } + } + else + { + nodeId = _nodeId.Value; + } + + if (MediaController.CheckPermissions( + context.HttpContext.Items, + _webSecurity.CurrentUser, + _mediaService, + _entityService, + nodeId)) + { + + } + else + { + throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + + } + + } + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs index 0044a3bd25..4152a7fd08 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs @@ -157,95 +157,14 @@ namespace Umbraco.Web.Editors.Binders { throw new ArgumentNullException(nameof(bindingContext)); } - var modelName = bindingContext.ModelName; - var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + var model = await _modelBinderHelper.BindModelFromMultipartRequestAsync(_jsonSerializer, _hostingEnvironment, bindingContext); - if (valueProviderResult == ValueProviderResult.None) - { - return; - } - bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); - - var value = valueProviderResult.FirstValue; - - // Check if the argument value is null or empty - if (string.IsNullOrEmpty(value)) - { - return; - } - var model = _jsonSerializer.Deserialize(value); if (model is null) { - // Non-integer arguments result in model state errors - bindingContext.ModelState.TryAddModelError( - modelName, $"Cannot deserialize {modelName} as {nameof(ContentItemSave)}."); - return; } - //Handle file uploads - foreach (var formFile in bindingContext.HttpContext.Request.Form.Files) - { - //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id - // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. - var parts = formFile.Name.Trim('\"').Split('_'); - if (parts.Length < 2) - { - 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 - string culture = null; - if (parts.Length > 2) - { - culture = parts[2]; - //normalize to null if empty - if (culture.IsNullOrWhiteSpace()) - { - culture = null; - } - } - - //if there are 4 parts part 4 is always segment - string segment = null; - if (parts.Length > 3) - { - segment = parts[3]; - //normalize to null if empty - if (segment.IsNullOrWhiteSpace()) - { - segment = null; - } - } - - // TODO: anything after 4 parts we can put in metadata - - var fileName = formFile.FileName.Trim('\"'); - - var tempFileUploadFolder = _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempFileUploads); - Directory.CreateDirectory(tempFileUploadFolder); - var tempFilePath = Path.Combine(tempFileUploadFolder, Guid.NewGuid().ToString()); - - using (var stream = System.IO.File.Create(tempFilePath)) - { - await formFile.CopyToAsync(stream); - } - - model.UploadedFiles.Add(new ContentPropertyFile - { - TempFilePath = tempFilePath, - PropertyAlias = propAlias, - Culture = culture, - Segment = segment, - FileName = fileName - }); - } - - - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); //create the dto from the persisted model @@ -270,5 +189,7 @@ namespace Umbraco.Web.Editors.Binders bindingContext.Result = ModelBindingResult.Success(model); } + + } } diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs index 21873f5a12..cfbf8e4027 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentModelBinderHelper.cs @@ -1,11 +1,14 @@ using System; using System.IO; using System.Net; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Net.Http.Headers; using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Core.Models.Editors; +using Umbraco.Core.Serialization; using Umbraco.Extensions; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Models.ContentEditing; @@ -17,73 +20,103 @@ namespace Umbraco.Web.Editors.Binders /// internal class ContentModelBinderHelper { - // public TModelSave BindModelFromMultipartRequest(ActionContext actionContext, - // ModelBindingContext bindingContext) - // where TModelSave : IHaveUploadedFiles - // { - // var result = actionContext.ReadAsMultipart(Constants.SystemDirectories.TempFileUploads); - // - // var model = actionContext.GetModelFromMultipartRequest(result, "contentItem"); - // - // //get the files - // foreach (var file in result.FileData) - // { - // //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id - // // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. - // var parts = file.Headers.ContentDisposition.Name.Trim('\"').Split('_'); - // if (parts.Length < 2) - // { - // 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 - // string culture = null; - // if (parts.Length > 2) - // { - // culture = parts[2]; - // //normalize to null if empty - // if (culture.IsNullOrWhiteSpace()) - // { - // culture = null; - // } - // } - // - // //if there are 4 parts part 4 is always segment - // string segment = null; - // if (parts.Length > 3) - // { - // segment = parts[3]; - // //normalize to null if empty - // if (segment.IsNullOrWhiteSpace()) - // { - // segment = null; - // } - // } - // - // // TODO: anything after 4 parts we can put in metadata - // - // var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); - // - // model.UploadedFiles.Add(new ContentPropertyFile - // { - // TempFilePath = file.LocalFileName, - // PropertyAlias = propAlias, - // Culture = culture, - // Segment = segment, - // FileName = fileName - // }); - // } - // - // bindingContext.Model = model; - // - // return model; - // } + public async Task BindModelFromMultipartRequestAsync( + IJsonSerializer jsonSerializer, + IHostingEnvironment hostingEnvironment, + ModelBindingContext bindingContext) + where T: class, IHaveUploadedFiles + { + var modelName = bindingContext.ModelName; - /// + var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + + if (valueProviderResult == ValueProviderResult.None) + { + return null; + } + bindingContext.ModelState.SetModelValue(modelName, valueProviderResult); + + var value = valueProviderResult.FirstValue; + + // Check if the argument value is null or empty + if (string.IsNullOrEmpty(value)) + { + return null; + } + var model = jsonSerializer.Deserialize(value); + if (model is null) + { + // Non-integer arguments result in model state errors + bindingContext.ModelState.TryAddModelError( + modelName, $"Cannot deserialize {modelName} as {nameof(T)}."); + + return null; + } + + //Handle file uploads + foreach (var formFile in bindingContext.HttpContext.Request.Form.Files) + { + //The name that has been assigned in JS has 2 or more parts. The second part indicates the property id + // for which the file belongs, the remaining parts are just metadata that can be used by the property editor. + var parts = formFile.Name.Trim('\"').Split('_'); + if (parts.Length < 2) + { + 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 + string culture = null; + if (parts.Length > 2) + { + culture = parts[2]; + //normalize to null if empty + if (culture.IsNullOrWhiteSpace()) + { + culture = null; + } + } + + //if there are 4 parts part 4 is always segment + string segment = null; + if (parts.Length > 3) + { + segment = parts[3]; + //normalize to null if empty + if (segment.IsNullOrWhiteSpace()) + { + segment = null; + } + } + + // TODO: anything after 4 parts we can put in metadata + + var fileName = formFile.FileName.Trim('\"'); + + var tempFileUploadFolder = hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.TempFileUploads); + Directory.CreateDirectory(tempFileUploadFolder); + var tempFilePath = Path.Combine(tempFileUploadFolder, Guid.NewGuid().ToString()); + + using (var stream = System.IO.File.Create(tempFilePath)) + { + await formFile.CopyToAsync(stream); + } + + model.UploadedFiles.Add(new ContentPropertyFile + { + TempFilePath = tempFilePath, + PropertyAlias = propAlias, + Culture = culture, + Segment = segment, + FileName = fileName + }); + } + + return model; + } + + /// /// we will now assign all of the values in the 'save' model to the DTO object /// /// diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs index a617d03625..7f7831eab2 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MediaItemBinder.cs @@ -1,78 +1,82 @@ -// using System; -// using System.Threading.Tasks; -// using Microsoft.AspNetCore.Mvc.ModelBinding; -// using Umbraco.Core.Mapping; -// using Umbraco.Core.Models; -// using Umbraco.Core.Services; -// using Umbraco.Web.Models.ContentEditing; -// -// namespace Umbraco.Web.Editors.Binders -// { -// /// -// /// The model binder for -// /// -// internal class MediaItemBinder : IModelBinder -// { -// private readonly IMediaService _mediaService; -// private readonly UmbracoMapper _umbracoMapper; -// private readonly IMediaTypeService _mediaTypeService; -// private readonly ContentModelBinderHelper _modelBinderHelper; -// -// -// public MediaItemBinder(IMediaService mediaService, UmbracoMapper umbracoMapper, IMediaTypeService mediaTypeService) -// { -// _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 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) -// { -// 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(); -// -// bindingContext.Result = ModelBindingResult.Success(model); -// return Task.CompletedTask; -// } -// -// private IMedia GetExisting(MediaItemSave model) -// { -// return _mediaService.GetById(Convert.ToInt32(model.Id)); -// } -// -// private IMedia CreateNew(MediaItemSave model) -// { -// var mediaType = _mediaTypeService.Get(model.ContentTypeAlias); -// if (mediaType == null) -// { -// throw new InvalidOperationException("No media type found with alias " + model.ContentTypeAlias); -// } -// return new Core.Models.Media(model.Name, model.ParentId, mediaType); -// } -// -// } -// } +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core.Hosting; +using Umbraco.Core.Mapping; +using Umbraco.Core.Models; +using Umbraco.Core.Serialization; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors.Binders +{ + /// + /// The model binder for + /// + internal class MediaItemBinder : IModelBinder + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IMediaService _mediaService; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMediaTypeService _mediaTypeService; + private readonly ContentModelBinderHelper _modelBinderHelper; + + + public MediaItemBinder(IJsonSerializer jsonSerializer, IHostingEnvironment hostingEnvironment, IMediaService mediaService, UmbracoMapper umbracoMapper, IMediaTypeService mediaTypeService) + { + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _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 async Task BindModelAsync(ModelBindingContext bindingContext) + { + + var model = await _modelBinderHelper.BindModelFromMultipartRequestAsync(_jsonSerializer, _hostingEnvironment, bindingContext); + if (model == null) + { + return; + } + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + 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(); + + bindingContext.Result = ModelBindingResult.Success(model); + } + + private IMedia GetExisting(MediaItemSave model) + { + return _mediaService.GetById(Convert.ToInt32(model.Id)); + } + + private IMedia CreateNew(MediaItemSave model) + { + var mediaType = _mediaTypeService.Get(model.ContentTypeAlias); + if (mediaType == null) + { + throw new InvalidOperationException("No media type found with alias " + model.ContentTypeAlias); + } + return new Core.Models.Media(model.Name, model.ParentId, mediaType); + } + + } +} diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs index 8e857a687f..23a4d3849f 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/MemberBinder.cs @@ -1,144 +1,148 @@ -// using System; -// using System.Collections.Generic; -// using Umbraco.Core; -// using Umbraco.Core.Models; -// using Umbraco.Core.Services; -// using Umbraco.Core.Strings; -// using Umbraco.Web.Models.ContentEditing; -// using System.Linq; -// using System.Threading.Tasks; -// using Microsoft.AspNetCore.Mvc.ModelBinding; -// using Umbraco.Core.Mapping; -// -// namespace Umbraco.Web.Editors.Binders -// { -// /// -// /// The model binder for -// /// -// internal class MemberBinder : IModelBinder -// { -// private readonly ContentModelBinderHelper _modelBinderHelper; -// private readonly IShortStringHelper _shortStringHelper; -// private readonly UmbracoMapper _umbracoMapper; -// private readonly IMemberService _memberService; -// private readonly IMemberTypeService _memberTypeService; -// -// public MemberBinder( -// IShortStringHelper shortStringHelper, -// UmbracoMapper umbracoMapper, -// IMemberService memberService, -// IMemberTypeService memberTypeService) -// { -// -// _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(); -// } -// -// /// -// /// 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) -// { -// 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(); -// -// bindingContext.Result = ModelBindingResult.Success(model); -// return Task.CompletedTask; -// } -// -// /// -// /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) -// /// -// /// -// /// -// private IMember GetExisting(MemberSave model) -// { -// return GetExisting(model.Key); -// } -// -// private IMember GetExisting(Guid key) -// { -// var member = _memberService.GetByKey(key); -// if (member == null) -// { -// throw new InvalidOperationException("Could not find member with key " + key); -// } -// -// return member; -// } -// -// /// -// /// Gets an instance of IMember used when creating a member -// /// -// /// -// /// -// /// -// /// Depending on whether a custom membership provider is configured this will return different results. -// /// -// private IMember CreateNew(MemberSave model) -// { -// var contentType = _memberTypeService.Get(model.ContentTypeAlias); -// if (contentType == null) -// { -// throw new InvalidOperationException("No member type found with alias " + model.ContentTypeAlias); -// } -// -// //remove all membership properties, these values are set with the membership provider. -// FilterMembershipProviderProperties(contentType); -// -// //return the new member with the details filled in -// return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType); -// } -// -// /// -// /// This will remove all of the special membership provider properties which were required to display the property editors -// /// for editing - but the values have been mapped back to the MemberSave object directly - we don't want to keep these properties -// /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist. -// /// -// /// -// private void FilterMembershipProviderProperties(IContentTypeBase contentType) -// { -// var defaultProps = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); -// //remove all membership properties, these values are set with the membership provider. -// var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); -// FilterContentTypeProperties(contentType, exclude); -// } -// -// private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude) -// { -// //remove all properties based on the exclusion list -// foreach (var remove in exclude) -// { -// if (contentType.PropertyTypeExists(remove)) -// { -// contentType.RemovePropertyType(remove); -// } -// } -// } -// -// -// } -// } +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Web.Models.ContentEditing; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core.Hosting; +using Umbraco.Core.Mapping; +using Umbraco.Core.Serialization; + +namespace Umbraco.Web.Editors.Binders +{ + /// + /// The model binder for + /// + internal class MemberBinder : IModelBinder + { + private readonly ContentModelBinderHelper _modelBinderHelper; + private readonly IJsonSerializer _jsonSerializer; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IShortStringHelper _shortStringHelper; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + + public MemberBinder( + IJsonSerializer jsonSerializer, + IHostingEnvironment hostingEnvironment, + IShortStringHelper shortStringHelper, + UmbracoMapper umbracoMapper, + IMemberService memberService, + IMemberTypeService memberTypeService) + { + _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _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(); + } + + /// + /// Creates the model from the request and binds it to the context + /// + /// + /// + /// + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + var model = await _modelBinderHelper.BindModelFromMultipartRequestAsync(_jsonSerializer, _hostingEnvironment, bindingContext); + if (model == null) + { + return; + } + + model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + + //create the dto from the persisted model + if (model.PersistedContent != null) + { + 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(); + + bindingContext.Result = ModelBindingResult.Success(model); + } + + /// + /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) + /// + /// + /// + private IMember GetExisting(MemberSave model) + { + return GetExisting(model.Key); + } + + private IMember GetExisting(Guid key) + { + var member = _memberService.GetByKey(key); + if (member == null) + { + throw new InvalidOperationException("Could not find member with key " + key); + } + + return member; + } + + /// + /// Gets an instance of IMember used when creating a member + /// + /// + /// + /// + /// Depending on whether a custom membership provider is configured this will return different results. + /// + private IMember CreateNew(MemberSave model) + { + var contentType = _memberTypeService.Get(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No member type found with alias " + model.ContentTypeAlias); + } + + //remove all membership properties, these values are set with the membership provider. + FilterMembershipProviderProperties(contentType); + + //return the new member with the details filled in + return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, contentType); + } + + /// + /// This will remove all of the special membership provider properties which were required to display the property editors + /// for editing - but the values have been mapped back to the MemberSave object directly - we don't want to keep these properties + /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist. + /// + /// + private void FilterMembershipProviderProperties(IContentTypeBase contentType) + { + var defaultProps = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + //remove all membership properties, these values are set with the membership provider. + var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); + FilterContentTypeProperties(contentType, exclude); + } + + private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude) + { + //remove all properties based on the exclusion list + foreach (var remove in exclude) + { + if (contentType.PropertyTypeExists(remove)) + { + contentType.RemovePropertyType(remove); + } + } + } + + + } +} diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs deleted file mode 100644 index cf96b0ade6..0000000000 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ /dev/null @@ -1,470 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using System.Web.Http; -using System.Web.Http.ModelBinding; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dictionary; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.ContentEditing; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Persistence; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; -using Umbraco.Core.Strings; -using Umbraco.Web.ContentApps; -using Umbraco.Web.Editors.Binders; -using Umbraco.Web.Editors.Filters; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Umbraco.Web.WebApi.Filters; -using Constants = Umbraco.Core.Constants; -using Umbraco.Core.Mapping; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Editors -{ - /// - /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting - /// access to ALL of the methods on this controller will need access to the member application. - /// - [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorize(Constants.Applications.Members)] - [OutgoingNoHyphenGuidFormat] - public class MemberController : ContentControllerBase - { - public MemberController( - IMemberPasswordConfiguration passwordConfig, - ICultureDictionary cultureDictionary, - PropertyEditorCollection propertyEditors, - IGlobalSettings globalSettings, - IUmbracoContextAccessor umbracoContextAccessor, - ISqlContext sqlContext, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger logger, - IRuntimeState runtimeState, - IShortStringHelper shortStringHelper, - UmbracoMapper umbracoMapper, - IPublishedUrlProvider publishedUrlProvider) - : base(cultureDictionary, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) - { - _passwordConfig = passwordConfig ?? throw new ArgumentNullException(nameof(passwordConfig)); - _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); - _passwordSecurity = new LegacyPasswordSecurity(_passwordConfig); - _passwordValidator = new ConfiguredPasswordValidator(); - } - - private readonly IMemberPasswordConfiguration _passwordConfig; - private readonly PropertyEditorCollection _propertyEditors; - private readonly LegacyPasswordSecurity _passwordSecurity; - private readonly IPasswordValidator _passwordValidator; - - public PagedResult GetPagedResults( - int pageNumber = 1, - int pageSize = 100, - string orderBy = "username", - Direction orderDirection = Direction.Ascending, - bool orderBySystemField = true, - string filter = "", - string memberTypeAlias = null) - { - - if (pageNumber <= 0 || pageSize <= 0) - { - throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); - } - - var members = Services.MemberService - .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); - if (totalRecords == 0) - { - return new PagedResult(0, 0, 0); - } - - var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) - { - Items = members - .Select(x => Mapper.Map(x)) - }; - return pagedResult; - } - - /// - /// Returns a display node with a list view to render members - /// - /// - /// - public MemberListDisplay GetListNodeDisplay(string listName) - { - var foundType = Services.MemberTypeService.Get(listName); - var name = foundType != null ? foundType.Name : listName; - - var apps = new List(); - apps.Add(ListViewContentAppFactory.CreateContentApp(Services.DataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); - apps[0].Active = true; - - var display = new MemberListDisplay - { - ContentTypeAlias = listName, - ContentTypeName = name, - Id = listName, - IsContainer = true, - Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, - Path = "-1," + listName, - ParentId = -1, - ContentApps = apps - }; - - return display; - } - - /// - /// Gets the content json for the member - /// - /// - /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core - public MemberDisplay GetByKey(Guid key) - { - var foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - HandleContentNotFound(key); - } - return Mapper.Map(foundMember); - } - - /// - /// Gets an empty content item for the - /// - /// - /// - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core - public MemberDisplay GetEmpty(string contentTypeAlias = null) - { - IMember emptyContent; - if (contentTypeAlias == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var contentType = Services.MemberTypeService.Get(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - var passwordGenerator = new PasswordGenerator(_passwordConfig); - - emptyContent = new Member(contentType); - emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); - return Mapper.Map(emptyContent); - } - - /// - /// Saves member - /// - /// - [FileUploadCleanupFilter] - // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core - [MemberSaveValidation] - public async Task PostSave( - [ModelBinder(typeof(MemberBinder))] - MemberSave contentItem) - { - - //If we've reached here it means: - // * Our model has been bound - // * and validated - // * any file attachments have been saved to their temporary location for us to use - // * we have a reference to the DTO object and the persisted object - // * Permissions are valid - - //map the properties to the persisted entity - MapPropertyValues(contentItem); - - await ValidateMemberDataAsync(contentItem); - - //Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors - if (ModelState.IsValid == false) - { - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } - - //We're gonna look up the current roles now because the below code can cause - // events to be raised and developers could be manually adding roles to members in - // their handlers. If we don't look this up now there's a chance we'll just end up - // removing the roles they've assigned. - var currRoles = Services.MemberService.GetAllRoles(contentItem.PersistedContent.Username); - //find the ones to remove and remove them - var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray(); - - //Depending on the action we need to first do a create or update using the membership provider - // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. - switch (contentItem.Action) - { - case ContentSaveAction.Save: - UpdateMemberData(contentItem); - break; - case ContentSaveAction.SaveNew: - contentItem.PersistedContent = CreateMemberData(contentItem); - break; - default: - //we don't support anything else for members - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - //TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope - // but it would be nicer to have this taken care of within the Save method itself - - //create/save the IMember - Services.MemberService.Save(contentItem.PersistedContent); - - //Now let's do the role provider stuff - now that we've saved the content item (that is important since - // if we are changing the username, it must be persisted before looking up the member roles). - if (rolesToRemove.Any()) - { - Services.MemberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove); - } - //find the ones to add and add them - var toAdd = contentItem.Groups.Except(currRoles).ToArray(); - if (toAdd.Any()) - { - //add the ones submitted - Services.MemberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); - } - - //return the updated model - var display = Mapper.Map(contentItem.PersistedContent); - - //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 - HandleInvalidModelState(display); - - var localizedTextService = Services.TextService; - //put the correct messages in - switch (contentItem.Action) - { - case ContentSaveAction.Save: - case ContentSaveAction.SaveNew: - display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); - break; - } - - return display; - } - - /// - /// Maps the property values to the persisted entity - /// - /// - private void MapPropertyValues(MemberSave contentItem) - { - UpdateName(contentItem); - - //map the custom properties - this will already be set for new entities in our member binder - contentItem.PersistedContent.Email = contentItem.Email; - contentItem.PersistedContent.Username = contentItem.Username; - - //use the base method to map the rest of the properties - base.MapPropertyValuesForPersistence( - contentItem, - contentItem.PropertyCollectionDto, - (save, property) => property.GetValue(), //get prop val - (save, property, v) => property.SetValue(v), //set prop val - null); // member are all invariant - } - - private IMember CreateMemberData(MemberSave contentItem) - { - var memberType = Services.MemberTypeService.Get(contentItem.ContentTypeAlias); - if (memberType == null) - throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}"); - var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true) - { - CreatorId = Security.CurrentUser.Id, - RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), - Comments = contentItem.Comments, - IsApproved = contentItem.IsApproved - }; - - return member; - } - - /// - /// Update the member security data - /// - /// - /// - /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. - /// - private void UpdateMemberData(MemberSave contentItem) - { - contentItem.PersistedContent.WriterId = Security.CurrentUser.Id; - - // If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types - // have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted. - // There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut - // but we will take care of this in a generic way below so that it works for all props. - if (!Security.CurrentUser.HasAccessToSensitiveData()) - { - var memberType = Services.MemberTypeService.Get(contentItem.PersistedContent.ContentTypeId); - var sensitiveProperties = memberType - .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) - .ToList(); - - foreach (var sensitiveProperty in sensitiveProperties) - { - var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); - if (destProp != null) - { - //if found, change the value of the contentItem model to the persisted value so it remains unchanged - var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias); - destProp.Value = origValue; - } - } - } - - var isLockedOut = contentItem.IsLockedOut; - - //if they were locked but now they are trying to be unlocked - if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) - { - contentItem.PersistedContent.IsLockedOut = false; - contentItem.PersistedContent.FailedPasswordAttempts = 0; - } - else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut) - { - //NOTE: This should not ever happen unless someone is mucking around with the request data. - //An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them - ModelState.AddModelError("custom", "An admin cannot lock a user"); - } - - //no password changes then exit ? - if (contentItem.Password == null) - return; - - // set the password - contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword); - } - - private static void UpdateName(MemberSave memberSave) - { - //Don't update the name if it is empty - if (memberSave.Name.IsNullOrWhiteSpace() == false) - { - memberSave.PersistedContent.Name = memberSave.Name; - } - } - - // TODO: This logic should be pulled into the service layer - private async Task ValidateMemberDataAsync(MemberSave contentItem) - { - if (contentItem.Name.IsNullOrWhiteSpace()) - { - ModelState.AddPropertyError( - new ValidationResult("Invalid user name", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return false; - } - - if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) - { - var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword); - if (!validPassword) - { - ModelState.AddPropertyError( - new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }), - string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return false; - } - } - - var byUsername = Services.MemberService.GetByUsername(contentItem.Username); - if (byUsername != null && byUsername.Key != contentItem.Key) - { - ModelState.AddPropertyError( - new ValidationResult("Username is already in use", new[] { "value" }), - string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return false; - } - - var byEmail = Services.MemberService.GetByEmail(contentItem.Email); - if (byEmail != null && byEmail.Key != contentItem.Key) - { - ModelState.AddPropertyError( - new ValidationResult("Email address is already in use", new[] { "value" }), - string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - return false; - } - - return true; - } - - /// - /// Permanently deletes a member - /// - /// - /// - /// - [HttpPost] - public HttpResponseMessage DeleteByKey(Guid key) - { - var foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - return HandleContentNotFound(key, false); - } - Services.MemberService.Delete(foundMember); - - return Request.CreateResponse(HttpStatusCode.OK); - } - - /// - /// Exports member data based on their unique Id - /// - /// The unique member identifier - /// - [HttpGet] - public HttpResponseMessage ExportMemberData(Guid key) - { - var currentUser = Security.CurrentUser; - - var httpResponseMessage = Request.CreateResponse(); - if (currentUser.HasAccessToSensitiveData() == false) - { - httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; - return httpResponseMessage; - } - - var member = ((MemberService)Services.MemberService).ExportMember(key); - - var fileName = $"{member.Name}_{member.Email}.txt"; - - httpResponseMessage.Content = new ObjectContent(member, new JsonMediaTypeFormatter { Indent = true }); - httpResponseMessage.Content.Headers.Add("x-filename", fileName); - httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); - httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; - httpResponseMessage.StatusCode = HttpStatusCode.OK; - - return httpResponseMessage; - } - } - - -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e582cff00d..a9a1838b3b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -308,7 +308,6 @@ - @@ -367,11 +366,9 @@ - - @@ -456,8 +453,5 @@ Mvc\web.config - - - \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs b/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs deleted file mode 100644 index 60e2889fd5..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/EnsureUserPermissionForMediaAttribute.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Web.Composing; -using Umbraco.Web.Editors; - -namespace Umbraco.Web.WebApi.Filters -{ - /// - /// Auth filter to check if the current user has access to the content item - /// - /// - /// Since media doesn't have permissions, this simply checks start node access - /// - internal sealed class EnsureUserPermissionForMediaAttribute : ActionFilterAttribute - { - private readonly int? _nodeId; - private readonly string _paramName; - - public enum DictionarySource - { - ActionArguments, - RequestForm, - RequestQueryString - } - - /// - /// This constructor will only be able to test the start node access - /// - public EnsureUserPermissionForMediaAttribute(int nodeId) - { - _nodeId = nodeId; - } - - public EnsureUserPermissionForMediaAttribute(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; - } - - // TODO: v8 guess this is not used anymore, source is ignored?! - public EnsureUserPermissionForMediaAttribute(string paramName, DictionarySource source) - { - if (paramName == null) throw new ArgumentNullException(nameof(paramName)); - if (string.IsNullOrEmpty(paramName)) throw new ArgumentException("Value can't be empty.", nameof(paramName)); - - _paramName = paramName; - } - - public override bool AllowMultiple => true; - - private int GetNodeIdFromParameter(object parameterValue) - { - if (parameterValue is int) - { - return (int) parameterValue; - } - - var guidId = Guid.Empty; - if (parameterValue is Guid) - { - guidId = (Guid)parameterValue; - } - else if (parameterValue is GuidUdi) - { - guidId = ((GuidUdi) parameterValue).Guid; - } - - if (guidId != Guid.Empty) - { - var found = Current.Services.EntityService.GetId(guidId, UmbracoObjectTypes.Media); - if (found) - return found.Result; - } - - throw new InvalidOperationException("The id type: " + parameterValue.GetType() + " is not a supported id"); - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - if (Current.UmbracoContext.Security.CurrentUser == null) - { - 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) - { - nodeId = GetNodeIdFromParameter(actionContext.ActionArguments[parts[0]]); - } - 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 = GetNodeIdFromParameter(prop.GetValue(actionContext.ActionArguments[parts[0]])); - } - } - else - { - nodeId = _nodeId.Value; - } - - if (MediaController.CheckPermissions( - actionContext.Request.Properties, - Current.UmbracoContext.Security.CurrentUser, - Current.Services.MediaService, - Current.Services.EntityService, - nodeId)) - { - base.OnActionExecuting(actionContext); - } - else - { - throw new HttpResponseException(System.Net.HttpStatusCode.Unauthorized); - } - } - } -}