diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index c679181963..d6133c04dc 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -37,5 +37,10 @@ namespace Umbraco.Core.Models.Membership /// The security stamp used by ASP.Net identity /// string SecurityStamp { get; set; } + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + string Avatar { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 9c444b48e4..e3c4d910af 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -95,6 +95,7 @@ namespace Umbraco.Core.Models.Membership private string _name; private string _securityStamp; + private string _avatar; private int _sessionTimeout; private int[] _startContentIds; private int[] _startMediaIds; @@ -124,6 +125,7 @@ namespace Umbraco.Core.Models.Membership public readonly PropertyInfo LastPasswordChangeDateSelector = ExpressionHelper.GetPropertyInfo(x => x.LastPasswordChangeDate); public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); + public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo(x => x.Avatar); public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds); public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds); @@ -272,6 +274,13 @@ namespace Umbraco.Core.Models.Membership set { SetPropertyValueAndDetectChanges(value, ref _securityStamp, Ps.Value.SecurityStampSelector); } } + [DataMember] + public string Avatar + { + get { return _avatar; } + set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); } + } + /// /// Gets or sets the session timeout. /// diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 0f74a92b7b..98dee12a94 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -79,6 +79,13 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } + + /// + /// Will hold the media file system relative path of the users custom avatar if they uploaded one + /// + [Column("avatar")] + [NullSetting(NullSetting = NullSettings.Null)] + public string Avatar { get; set; } [ResultColumn] public List UserGroupDtos { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index c4154a7908..64a63447d9 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -44,6 +44,7 @@ namespace Umbraco.Core.Persistence.Factories user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue; user.CreateDate = dto.CreateDate; user.UpdateDate = dto.UpdateDate; + user.Avatar = dto.Avatar; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 @@ -74,7 +75,8 @@ namespace Umbraco.Core.Persistence.Factories LastLoginDate = entity.LastLoginDate == DateTime.MinValue ? (DateTime?)null : entity.LastLoginDate, LastPasswordChangeDate = entity.LastPasswordChangeDate == DateTime.MinValue ? (DateTime?)null : entity.LastPasswordChangeDate, CreateDate = entity.CreateDate, - UpdateDate = entity.UpdateDate + UpdateDate = entity.UpdateDate, + Avatar = entity.Avatar }; foreach (var startNodeId in entity.StartContentIds) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js index 158640cff0..c70729246f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/users.resource.js @@ -9,7 +9,7 @@ (function () { 'use strict'; - function usersResource($http, umbRequestHelper, $q) { + function usersResource($http, umbRequestHelper, $q, umbDataFormatter) { function disableUsers(userIds) { if (!userIds) { @@ -121,15 +121,18 @@ throw "user not specified"; } + //need to convert the user data into the correctly formatted save data - it is *not* the same and we don't want to over-post + var formattedSaveData = umbDataFormatter.formatUserPostData(user); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "userApiBaseUrl", "PostSaveUser"), - user), + formattedSaveData), "Failed to save user"); } - + function getUserGroup() { var deferred = $q.defer(); var user = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index ba395becfc..f093d4bb31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -4,7 +4,7 @@ function versionHelper() { return { //see: https://gist.github.com/TheDistantSea/8021359 - versionCompare: function(v1, v2, options) { + versionCompare: function (v1, v2, options) { var lexicographical = options && options.lexicographical, zeroExtend = options && options.zeroExtend, v1parts = v1.split('.'), @@ -61,15 +61,15 @@ angular.module('umbraco.services').factory('versionHelper', versionHelper); function dateHelper() { return { - - convertToServerStringTime: function(momentLocal, serverOffsetMinutes, format) { + + convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) { //get the formatted offset time in HH:mm (server time offset is in minutes) var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); var server = moment.utc(momentLocal).utcOffset(formattedOffset); return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); @@ -80,9 +80,9 @@ function dateHelper() { //get the formatted offset time in HH:mm (server time offset is in minutes) var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); //convert to the iso string format var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; @@ -121,30 +121,30 @@ angular.module('umbraco.services').factory('packageHelper', packageHelper); function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { return { /** sets the image's url, thumbnail and if its a folder */ - setImageData: function(img) { - + setImageData: function (img) { + img.isFolder = !mediaHelper.hasFilePropertyType(img); - if(!img.isFolder){ + if (!img.isFolder) { img.thumbnail = mediaHelper.resolveFile(img, true); - img.image = mediaHelper.resolveFile(img, false); + img.image = mediaHelper.resolveFile(img, false); } }, /** sets the images original size properties - will check if it is a folder and if so will just make it square */ - setOriginalSize: function(img, maxHeight) { + setOriginalSize: function (img, maxHeight) { //set to a square by default img.originalWidth = maxHeight; img.originalHeight = maxHeight; - var widthProp = _.find(img.properties, function(v) { return (v.alias === "umbracoWidth"); }); + var widthProp = _.find(img.properties, function (v) { return (v.alias === "umbracoWidth"); }); if (widthProp && widthProp.value) { img.originalWidth = parseInt(widthProp.value, 10); if (isNaN(img.originalWidth)) { img.originalWidth = maxHeight; } } - var heightProp = _.find(img.properties, function(v) { return (v.alias === "umbracoHeight"); }); + var heightProp = _.find(img.properties, function (v) { return (v.alias === "umbracoHeight"); }); if (heightProp && heightProp.value) { img.originalHeight = parseInt(heightProp.value, 10); if (isNaN(img.originalHeight)) { @@ -154,7 +154,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** sets the image style which get's used in the angular markup */ - setImageStyle: function(img, width, height, rightMargin, bottomMargin) { + setImageStyle: function (img, width, height, rightMargin, bottomMargin) { img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; img.thumbStyle = { "background-image": "url('" + img.thumbnail + "')", @@ -162,10 +162,10 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me "background-position": "center", "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" }; - }, + }, /** gets the image's scaled wdith based on the max row height */ - getScaledWidth: function(img, maxHeight) { + getScaledWidth: function (img, maxHeight) { var scaled = img.originalWidth * maxHeight / img.originalHeight; return scaled; //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row @@ -173,7 +173,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ - getTargetWidth: function(imgsPerRow, maxRowWidth, margin) { + getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { //take into account the margin, we will have 1 less margin item than we have total images return (maxRowWidth - ((imgsPerRow - 1) * margin)); }, @@ -187,7 +187,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me targetHeight = optional; */ - getRowHeightForImages: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { + getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { var idealImages = imgs.slice(0, idealImgPerRow); //get the target row width without margin @@ -195,8 +195,8 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //this gets the image with the smallest height which equals the maximum we can scale up for this image block var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); - + targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); + var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); if (attemptedRowHeight != null) { @@ -206,12 +206,12 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (attemptedRowHeight < minDisplayHeight) { if (idealImages.length > 1) { - + //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); } - else { + else { //this will occur when we only have one image remaining in the row but it's still going to be too wide even when // using the minimum display height specified. In this case we're going to have to just crop the image in it's center // using the minimum display height and the full row width @@ -241,7 +241,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) - + while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); @@ -273,7 +273,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, - performGetRowHeight: function(idealImages, targetRowWidth, minDisplayHeight, targetHeight) { + performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { var currRowWidth = 0; @@ -285,7 +285,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (currRowWidth > targetRowWidth) { //get the new scaled height to fit var newHeight = targetRowWidth * targetHeight / currRowWidth; - + return newHeight; } else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { @@ -303,7 +303,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** builds an image grid row */ - buildRow: function(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { + buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { var currRowWidth = 0; var row = { images: [] }; @@ -315,11 +315,11 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me for (var i = 0; i < imageRowHeight.imgCount; i++) { //get the lower width to ensure it always fits var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - + if (currRowWidth + scaledWidth <= targetWidth) { - currRowWidth += scaledWidth; + currRowWidth += scaledWidth; sizes.push({ - width:scaledWidth, + width: scaledWidth, //ensure that the height is rounded height: Math.round(imageRowHeight.height) }); @@ -352,17 +352,17 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me if (row.images.length === 1 && totalRemaining > 1) { //if there's only one image on the row and there are more images remaining, set the container to max width - row.images[0].style.width = maxRowWidth + "px"; + row.images[0].style.width = maxRowWidth + "px"; } - + return row; }, /** Returns the maximum image scaling height for the current image collection */ - getMaxScaleableHeight: function(imgs, maxRowHeight) { + getMaxScaleableHeight: function (imgs, maxRowHeight) { - var smallestHeight = _.min(imgs, function(item) { return item.originalHeight; }).originalHeight; + var smallestHeight = _.min(imgs, function (item) { return item.originalHeight; }).originalHeight; //adjust the smallestHeight if it is larger than the static max row height if (smallestHeight > maxRowHeight) { @@ -372,10 +372,10 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me }, /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ - buildGrid: function(images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin,imagesOnly) { + buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { var rows = []; - var imagesProcessed = 0; + var imagesProcessed = 0; //first fill in all of the original image sizes and URLs for (var i = startingIndex; i < images.length; i++) { @@ -384,7 +384,7 @@ function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, me this.setImageData(item); this.setOriginalSize(item, maxRowHeight); - if(imagesOnly && !item.isFolder && !item.thumbnail){ + if (imagesOnly && !item.isFolder && !item.thumbnail) { images.splice(i, 1); i--; } @@ -449,7 +449,7 @@ function umbModelMapper() { /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ convertToEntityBasic: function (source) { - var required = ["id", "name", "icon", "parentId", "path"]; + var required = ["id", "name", "icon", "parentId", "path"]; _.each(required, function (k) { if (!_.has(source, k)) { throw "The source object does not contain the property " + k; @@ -485,11 +485,11 @@ function umbSessionStorage($window) { get: function (key) { return angular.fromJson(storage["umb_" + key]); }, - - set : function(key, value) { + + set: function (key, value) { storage["umb_" + key] = angular.toJson(value); } - + }; } angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage); @@ -504,26 +504,26 @@ angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorag */ function updateChecker($http, umbRequestHelper) { return { - - /** - * @ngdoc function - * @name umbraco.services.updateChecker#check - * @methodOf umbraco.services.updateChecker - * @function - * - * @description - * Called to load in the legacy tree js which is required on startup if a user is logged in or - * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. - */ - check: function() { - + + /** + * @ngdoc function + * @name umbraco.services.updateChecker#check + * @methodOf umbraco.services.updateChecker + * @function + * + * @description + * Called to load in the legacy tree js which is required on startup if a user is logged in or + * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. + */ + check: function () { + return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "updateCheckApiBaseUrl", - "GetCheck")), - 'Failed to retrieve update status'); - } + $http.get( + umbRequestHelper.getApiUrl( + "updateCheckApiBaseUrl", + "GetCheck")), + 'Failed to retrieve update status'); + } }; } angular.module('umbraco.services').factory('updateChecker', updateChecker); @@ -546,7 +546,7 @@ function umbPropEditorHelper() { * * @param {string} input the view path currently stored for the property editor */ - getViewPath: function(input, isPreValue) { + getViewPath: function (input, isPreValue) { var path = String(input); if (path.startsWith('/')) { @@ -582,7 +582,7 @@ angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorH **/ function umbDataFormatter() { return { - + formatContentTypePostData: function (displayModel, action) { //create the save model from the display model @@ -594,7 +594,7 @@ function umbDataFormatter() { //TODO: Map these saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; - var realGroups = _.reject(displayModel.groups, function(g) { + var realGroups = _.reject(displayModel.groups, function (g) { //do not include these tabs return g.tabState === "init"; }); @@ -621,9 +621,9 @@ function umbDataFormatter() { return saveGroup; }); - + //we don't want any null groups - saveModel.groups = _.reject(saveModel.groups, function(g) { + saveModel.groups = _.reject(saveModel.groups, function (g) { return !g; }); @@ -631,7 +631,7 @@ function umbDataFormatter() { }, /** formats the display model used to display the data type to the model used to save the data type */ - formatDataTypePostData: function(displayModel, preValues, action) { + formatDataTypePostData: function (displayModel, preValues, action) { var saveModel = { parentId: displayModel.parentId, id: displayModel.id, @@ -651,14 +651,23 @@ function umbDataFormatter() { return saveModel; }, + /** formats the display model used to display the user to the model used to save the user */ + formatUserPostData: function (displayModel, preValues, action) { + + //create the save model from the display model + var saveModel = _.pick(displayModel, 'id', 'parentId', 'name', 'username', 'culture', 'email', 'startContentIds', 'startMediaIds', 'userGroups'); + + return saveModel; + }, + /** formats the display model used to display the member to the model used to save the member */ - formatMemberPostData: function(displayModel, action) { + formatMemberPostData: function (displayModel, action) { //this is basically the same as for media but we need to explicitly add the username,email, password to the save model var saveModel = this.formatMediaPostData(displayModel, action); saveModel.key = displayModel.key; - + var genericTab = _.find(displayModel.tabs, function (item) { return item.id === 0; }); @@ -679,7 +688,7 @@ function umbDataFormatter() { saveModel.email = propEmail.value; saveModel.username = propLogin.value; saveModel.password = propPass.value; - + var selectedGroups = []; for (var n in propGroups.value) { if (propGroups.value[n] === true) { @@ -687,12 +696,12 @@ function umbDataFormatter() { } } saveModel.memberGroups = selectedGroups; - + //turn the dictionary into an array of pairs var memberProviderPropAliases = _.pairs(displayModel.fieldConfig); _.each(displayModel.tabs, function (tab) { _.each(tab.properties, function (prop) { - var foundAlias = _.find(memberProviderPropAliases, function(item) { + var foundAlias = _.find(memberProviderPropAliases, function (item) { return prop.alias === item[1]; }); if (foundAlias) { @@ -709,7 +718,7 @@ function umbDataFormatter() { saveModel.comments = prop.value; break; } - } + } }); }); @@ -719,7 +728,7 @@ function umbDataFormatter() { }, /** formats the display model used to display the media to the model used to save the media */ - formatMediaPostData: function(displayModel, action) { + formatMediaPostData: function (displayModel, action) { //NOTE: the display model inherits from the save model so we can in theory just post up the display model but // we don't want to post all of the data as it is unecessary. var saveModel = { @@ -744,7 +753,7 @@ function umbDataFormatter() { value: prop.value }); } - + }); }); @@ -760,8 +769,8 @@ function umbDataFormatter() { var genericTab = _.find(displayModel.tabs, function (item) { return item.id === 0; }); - - var propExpireDate = _.find(genericTab.properties, function(item) { + + var propExpireDate = _.find(genericTab.properties, function (item) { return item.alias === "_umb_expiredate"; }); var propReleaseDate = _.find(genericTab.properties, function (item) { diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 0e74de8af1..9bb136b31c 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Threading.Tasks; using System.Web; using System.Web.Http; using AutoMapper; using ClientDependency.Core; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -42,13 +45,89 @@ namespace Umbraco.Web.Editors { } - public IHttpActionResult SetAvatar(int id) + [FileUploadCleanupFilter(false)] + public async Task SetAvatar(int id) { - return Ok(); + 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 + var userId = result.FormData["userId"]; + int intUserId; + if (int.TryParse(userId, out intUserId) == false) + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the userId is not an integer"); + + var user = Services.UserService.GetUserById(intUserId); + if (user == null) + return Request.CreateResponse(HttpStatusCode.NotFound); + + var tempFiles = new PostedFiles(); + + if (result.FileData.Count > 1) + return Request.CreateValidationErrorResponse("The request was not formatted correctly, only one file can be attached to the request"); + + //get the file info + var file = result.FileData[0]; + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd(); + var safeFileName = fileName.ToSafeFileName(); + var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower(); + + if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) + { + if (user.Avatar.IsNullOrWhiteSpace()) + { + //we'll need to generate a new path! + //make it a hash of known data, we don't want this path to be guessable + user.Avatar = "UserAvatars/" + (user.Id + user.CreateDate.ToString("yyyyMMdd")).ToSHA1() + ext; + } + + using (var fs = System.IO.File.OpenRead(file.LocalFileName)) + { + FileSystemProviderManager.Current.MediaFileSystem.AddFile(user.Avatar, fs, true); + } + + Services.UserService.Save(user); + + //track the temp file so the cleanup filter removes it + tempFiles.UploadedFiles.Add(new ContentItemFile + { + TempFilePath = file.LocalFileName + }); + } + + return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } public IHttpActionResult ClearAvatar(int id) { + var found = Services.UserService.GetUserById(id); + if (found == null) + return NotFound(); + + var filePath = found.Avatar; + + found.Avatar = null; + + Services.UserService.Save(found); + + if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath)) + FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath); + return Ok(); } diff --git a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs index 8e7b247b23..4b408d324e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserSave.cs @@ -42,10 +42,7 @@ namespace Umbraco.Web.Models.ContentEditing public int[] StartContentIds { get; set; } [DataMember(Name = "startMediaIds")] - public int[] StartMediaIds { get; set; } - - [DataMember(Name = "allowedSections")] - public IEnumerable AllowedSections { get; set; } + public int[] StartMediaIds { get; set; } public IEnumerable Validate(ValidationContext validationContext) { diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 731d9037f3..aee0ae8336 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -19,6 +19,7 @@ namespace Umbraco.Web.Models.Mapping //Used for merging existing UserSave to an existing IUser instance - this will not create an IUser instance! config.CreateMap() .ForMember(user => user.Language, expression => expression.MapFrom(save => save.Culture)) + .ForMember(user => user.Avatar, expression => expression.Ignore()) .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) .ForMember(user => user.SecurityStamp, expression => expression.Ignore()) .ForMember(user => user.ProviderUserKey, expression => expression.Ignore()) @@ -47,6 +48,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ConstructUsing(invite => new User(invite.Name, invite.Email, invite.Email, Guid.NewGuid().ToString("N"))) .ForMember(user => user.Id, expression => expression.Ignore()) + .ForMember(user => user.Avatar, expression => expression.Ignore()) .ForMember(user => user.SessionTimeout, expression => expression.Ignore()) .ForMember(user => user.StartContentIds, expression => expression.Ignore()) .ForMember(user => user.StartMediaIds, expression => expression.Ignore())