using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Security.AccessControl; using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Dynamics; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Mapping; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; using System.Linq; using System.Runtime.Serialization; using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; using umbraco; using umbraco.BusinessLogic.Actions; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Web.UI; using Notification = Umbraco.Web.Models.ContentEditing.Notification; 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 media application. /// [PluginController("UmbracoApi")] [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Media)] public class MediaController : ContentControllerBase { /// /// Constructor /// public MediaController() : this(UmbracoContext.Current) { } /// /// Constructor /// /// public MediaController(UmbracoContext umbracoContext) : base(umbracoContext) { } /// /// Gets an empty content item for the /// /// /// /// public MediaItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetMediaType(contentTypeAlias); if (contentType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var emptyContent = Services.MediaService.CreateMedia("", parentId, contentType.Alias, UmbracoUser.Id); var mapped = Mapper.Map(emptyContent); //remove this tab if it exists: umbContainerView var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); return mapped; } /// /// Gets the content json for the content id /// /// /// [EnsureUserPermissionForMedia("id")] public MediaItemDisplay GetById(int id) { var foundContent = GetObjectFromRequest(() => Services.MediaService.GetById(id)); if (foundContent == null) { HandleContentNotFound(id); //HandleContentNotFound will throw an exception return null; } return Mapper.Map(foundContent); } /// /// Return media for the specified ids /// /// /// [FilterAllowedOutgoingMedia(typeof(IEnumerable))] public IEnumerable GetByIds([FromUri]int[] ids) { var foundMedia = Services.MediaService.GetByIds(ids); return foundMedia.Select(Mapper.Map); } /// /// Returns the root media objects /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>))] public IEnumerable> GetRootMedia() { //TODO: Add permissions check! return Services.MediaService.GetRootMedia() .Select(Mapper.Map>); } /// /// Returns the child media objects /// [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] public PagedResult> GetChildren(int id, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", Direction orderDirection = Direction.Ascending, string filter = "") { int totalChildren; IMedia[] children; if (pageNumber > 0 && pageSize > 0) { children = Services.MediaService.GetPagedChildren(id, (pageNumber - 1), pageSize, out totalChildren, orderBy, orderDirection, filter).ToArray(); } else { children = Services.MediaService.GetChildren(id).ToArray(); totalChildren = children.Length; } if (totalChildren == 0) { return new PagedResult>(0, 0, 0); } var pagedResult = new PagedResult>(totalChildren, pageNumber, pageSize); pagedResult.Items = children .Select(Mapper.Map>); return pagedResult; } /// /// Moves an item to the recycle bin, if it is already there then it will permanently delete it /// /// /// [EnsureUserPermissionForMedia("id")] [HttpPost] public HttpResponseMessage DeleteById(int id) { var foundMedia = GetObjectFromRequest(() => Services.MediaService.GetById(id)); if (foundMedia == null) { return HandleContentNotFound(id, false); } //if the current item is in the recycle bin if (foundMedia.IsInRecycleBin() == false) { var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); } } else { var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); } } return Request.CreateResponse(HttpStatusCode.OK); } /// /// Change the sort order for media /// /// /// [EnsureUserPermissionForMedia("move.Id")] public HttpResponseMessage PostMove(MoveOrCopy move) { var toMove = ValidateMoveOrCopy(move); Services.MediaService.Move(toMove, move.ParentId); var response = Request.CreateResponse(HttpStatusCode.OK); response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); return response; } /// /// Saves content /// /// [FileUploadCleanupFilter] [MediaPostValidate] public MediaItemDisplay PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave 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 MapPropertyValues(contentItem); //We need to manually check the validation results here because: // * We still need to save the entity even if there are validation value errors // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) // then we cannot continue saving, we can only display errors // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display // a message indicating this if (ModelState.IsValid == false) { if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) && (contentItem.Action == ContentSaveAction.SaveNew)) { //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! // add the modelstate to the outgoing object and throw validation response var forDisplay = Mapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } } //save the item var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); //return the updated model var display = Mapper.Map(contentItem.PersistedContent); //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); //put the correct msgs in switch (contentItem.Action) { case ContentSaveAction.Save: case ContentSaveAction.SaveNew: if (saveStatus.Success) { display.AddSuccessNotification( Services.TextService.Localize("speechBubbles/editMediaSaved"), Services.TextService.Localize("speechBubbles/editMediaSavedText")); } else { AddCancelMessage(display); } break; } return display; } /// /// Maps the property values to the persisted entity /// /// protected override void MapPropertyValues(ContentBaseItemSave contentItem) { UpdateName(contentItem); //use the base method to map the rest of the properties base.MapPropertyValues(contentItem); } /// /// Empties the recycle bin /// /// [HttpDelete] [HttpPost] public HttpResponseMessage EmptyRecycleBin() { Services.MediaService.EmptyRecycleBin(); return Request.CreateResponse(HttpStatusCode.OK); } /// /// Change the sort order for media /// /// /// [EnsureUserPermissionForMedia("sorted.ParentId")] public HttpResponseMessage PostSort(ContentSortOrder sorted) { if (sorted == null) { return Request.CreateResponse(HttpStatusCode.NotFound); } //if there's nothing to sort just return ok if (sorted.IdSortOrder.Length == 0) { return Request.CreateResponse(HttpStatusCode.OK); } var mediaService = base.ApplicationContext.Services.MediaService; var sortedMedia = new List(); try { sortedMedia.AddRange(sorted.IdSortOrder.Select(mediaService.GetById)); // Save Media with new sort order and update content xml in db accordingly if (mediaService.Sort(sortedMedia) == false) { LogHelper.Warn("Media sorting failed, this was probably caused by an event being cancelled"); return Request.CreateValidationErrorResponse("Media sorting failed, this was probably caused by an event being cancelled"); } return Request.CreateResponse(HttpStatusCode.OK); } catch (Exception ex) { LogHelper.Error("Could not update media sort order", ex); throw; } } [EnsureUserPermissionForMedia("folder.ParentId")] public MediaItemDisplay PostAddFolder(EntityBasic folder) { var mediaService = ApplicationContext.Services.MediaService; var f = mediaService.CreateMedia(folder.Name, folder.ParentId, Constants.Conventions.MediaTypes.Folder); mediaService.Save(f, Security.CurrentUser.Id); return Mapper.Map(f); } /// /// Used to submit a media file /// /// /// /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. /// [FileUploadCleanupFilter(false)] public async Task PostAddFile() { if (Request.Content.IsMimeMultipartContent() == false) { throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); } var root = IOHelper.MapPath("~/App_Data/TEMP/FileUploads"); //ensure it exists Directory.CreateDirectory(root); var provider = new MultipartFormDataStreamProvider(root); var result = await Request.Content.ReadAsMultipartAsync(provider); //must have a file if (result.FileData.Count == 0) { return Request.CreateResponse(HttpStatusCode.NotFound); } //get the string json from the request int parentId; if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) { return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); } //ensure the user has access to this folder by parent id! if (CheckPermissions( new Dictionary(), Security.CurrentUser, Services.MediaService, parentId) == false) { return Request.CreateResponse( HttpStatusCode.Unauthorized, new SimpleNotificationModel(new Notification( Services.TextService.Localize("speechBubbles/operationFailedHeader"), Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), SpeechBubbleIcon.Warning))); } var tempFiles = new PostedFiles(); //get the files foreach (var file in result.FileData) { var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); var ext = fileName.Substring(fileName.LastIndexOf('.')+1).ToLower(); if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) { var mediaType = Constants.Conventions.MediaTypes.File; if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) mediaType = Constants.Conventions.MediaTypes.Image; var mediaService = ApplicationContext.Services.MediaService; var f = mediaService.CreateMedia(fileName, parentId, mediaType, Security.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(Constants.Conventions.Media.File, fileName, fs); } var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); if (saveResult == false) { AddCancelMessage(tempFiles, message: Services.TextService.Localize("speechBubbles/operationCancelledText") + " -- " + fileName, localizeMessage: false); } else { tempFiles.UploadedFiles.Add(new ContentItemFile { FileName = fileName, PropertyAlias = Constants.Conventions.Media.File, TempFilePath = file.LocalFileName }); } } else { tempFiles.Notifications.Add(new Notification( Services.TextService.Localize("speechBubbles/operationFailedHeader"), "Cannot upload file " + file + ", it is not an approved file type", SpeechBubbleIcon.Warning)); } } //Different response if this is a 'blueimp' request if (Request.GetQueryNameValuePairs().Any(x => x.Key == "origin")) { var origin = Request.GetQueryNameValuePairs().First(x => x.Key == "origin"); if (origin.Value == "blueimp") { return Request.CreateResponse(HttpStatusCode.OK, tempFiles, //Don't output the angular xsrf stuff, blue imp doesn't like that new JsonMediaTypeFormatter()); } } return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } /// /// This is used for the response of PostAddFile so that we can analyze the response in a filter and remove the /// temporary files that were created. /// [DataContract] private class PostedFiles : IHaveUploadedFiles, INotificationModel { public PostedFiles() { UploadedFiles = new List(); Notifications = new List(); } public List UploadedFiles { get; private set; } [DataMember(Name = "notifications")] public List Notifications { get; private set; } } /// /// Ensures the item can be moved/copied to the new location /// /// /// private IMedia ValidateMoveOrCopy(MoveOrCopy model) { if (model == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var mediaService = Services.MediaService; var toMove = mediaService.GetById(model.Id); if (toMove == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } if (model.ParentId < 0) { //cannot move if the content item is not allowed at the root if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedAtRoot", Security.CurrentUser))); } } else { var parent = mediaService.GetById(model.ParentId); if (parent == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } //check if the item is allowed under this one if (parent.ContentType.AllowedContentTypes.Select(x => x.Id).ToArray() .Any(x => x.Value == toMove.ContentType.Id) == false) { throw new HttpResponseException( Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByContentType", Security.CurrentUser))); } // Check on paths if ((string.Format(",{0},", parent.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) { throw new HttpResponseException( Request.CreateValidationErrorResponse(ui.Text("moveOrCopy", "notAllowedByPath", Security.CurrentUser))); } } return toMove; } /// /// Performs a permissions check for the user to check if it has access to the node based on /// start node and/or permissions for the node /// /// The storage to add the content item to so it can be reused /// /// /// The content to lookup, if the contentItem is not specified /// Specifies the already resolved content item to check against, setting this ignores the nodeId /// internal static bool CheckPermissions(IDictionary storage, IUser user, IMediaService mediaService, int nodeId, IMedia media = null) { if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { media = mediaService.GetById(nodeId); //put the content item into storage so it can be retreived // in the controller (saves a lookup) storage[typeof(IMedia).ToString()] = media; } if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { throw new HttpResponseException(HttpStatusCode.NotFound); } var hasPathAccess = (nodeId == Constants.System.Root) ? UserExtensions.HasPathAccess( Constants.System.Root.ToInvariantString(), user.StartMediaId, Constants.System.RecycleBinMedia) : (nodeId == Constants.System.RecycleBinMedia) ? UserExtensions.HasPathAccess( Constants.System.RecycleBinMedia.ToInvariantString(), user.StartMediaId, Constants.System.RecycleBinMedia) : user.HasPathAccess(media); return hasPathAccess; } } }