wires up custom user avatars

This commit is contained in:
Shannon
2017-05-26 02:15:37 +10:00
parent 528be48437
commit 8aabb3e3d0
15 changed files with 237 additions and 126 deletions

View File

@@ -1,6 +1,9 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using Umbraco.Core.Cache;
using Umbraco.Core.IO;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
@@ -8,6 +11,69 @@ namespace Umbraco.Core.Models
{
public static class UserExtensions
{
/// <summary>
/// Tries to lookup the user's gravatar to see if the endpoint can be reached, if so it returns the valid URL
/// </summary>
/// <param name="user"></param>
/// <param name="userService"></param>
/// <param name="staticCache"></param>
/// <returns>
/// A list of 5 different sized avatar URLs
/// </returns>
internal static string[] GetCurrentUserAvatarUrls(this IUser user, IUserService userService, ICacheProvider staticCache)
{
if (user.Avatar.IsNullOrWhiteSpace())
{
var gravatarHash = user.Email.ToMd5();
var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash;
//try gravatar
var gravatarAccess = staticCache.GetCacheItem<bool>("UserAvatar" + user.Id, () =>
{
// Test if we can reach this URL, will fail when there's network or firewall errors
var request = (HttpWebRequest)WebRequest.Create(gravatarUrl);
// Require response within 10 seconds
request.Timeout = 10000;
try
{
using ((HttpWebResponse)request.GetResponse()) { }
}
catch (Exception)
{
// There was an HTTP or other error, return an null instead
return false;
}
return true;
});
if (gravatarAccess)
{
return new[]
{
gravatarUrl + "?s=30&d=mm",
gravatarUrl + "?s=60&d=mm",
gravatarUrl + "?s=90&d=mm",
gravatarUrl + "?s=150&d=mm",
gravatarUrl + "?s=300&d=mm"
};
}
return null;
}
//use the custom avatar
var avatarUrl = FileSystemProviderManager.Current.MediaFileSystem.GetUrl(user.Avatar);
return new[]
{
avatarUrl + "?width=30&height=30&mode=crop",
avatarUrl + "?width=60&height=60&mode=crop",
avatarUrl + "?width=90&height=90&mode=crop",
avatarUrl + "?width=150&height=150&mode=crop",
avatarUrl + "?width=300&height=300&mode=crop"
};
}
/// <summary>
/// Returns the culture info associated with this user, based on the language they're assigned to in the back office
/// </summary>

View File

@@ -269,7 +269,8 @@ namespace Umbraco.Core.Persistence.Repositories
{"lastLoginDate", "LastLoginDate"},
{"failedLoginAttempts", "FailedPasswordAttempts"},
{"createDate", "CreateDate"},
{"updateDate", "UpdateDate"}
{"updateDate", "UpdateDate"},
{"avatar", "Avatar"}
};
//create list of properties that have changed

View File

@@ -11,6 +11,17 @@
function usersResource($http, umbRequestHelper, $q, umbDataFormatter) {
function clearAvatar(userId) {
return umbRequestHelper.resourcePromise(
$http.post(
umbRequestHelper.getApiUrl(
"userApiBaseUrl",
"PostClearAvatar",
{ id: userId })),
'Failed to clear the user avatar ' + userId);
}
function disableUsers(userIds) {
if (!userIds) {
throw "userIds not specified";
@@ -188,7 +199,8 @@
createUser: createUser,
saveUser: saveUser,
getUserGroup: getUserGroup,
getUserGroups: getUserGroups
getUserGroups: getUserGroups,
clearAvatar: clearAvatar
};
return resource;

View File

@@ -60,7 +60,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $
$scope.authenticated = data.authenticated;
$scope.user = data.user;
updateChecker.check().then(function(update) {
updateChecker.check().then(function (update) {
if (update && update !== "null") {
if (update.type !== "None") {
var notification = {
@@ -87,7 +87,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $
}
//if this is a new login (i.e. the user entered credentials), then clear out local storage - could contain sensitive data
if (data.loginType === "credentials") {
if (data.loginType === "credentials") {
localStorageService.clearAll();
}
@@ -96,30 +96,15 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $
tmhDynamicLocale.set($scope.user.locale);
}
if ($scope.user.emailHash) {
if ($scope.user.avatars) {
//let's attempt to load the avatar, it might not exist or we might not have
// internet access, well get an empty string back
$http.get(umbRequestHelper.getApiUrl("gravatarApiBaseUrl", "GetCurrentUserGravatarUrl"))
.then(
function successCallback(response) {
// if we can't download the gravatar for some reason, an null gets returned, we cannot do anything
if (response.data !== "null") {
if ($scope.user && $scope.user.emailHash) {
var avatarBaseUrl = "https://www.gravatar.com/avatar/";
var hash = $scope.user.emailHash;
$scope.avatar = [];
if (angular.isArray($scope.user.avatars)) {
for (var i = 0; i < $scope.user.avatars.length; i++) {
$scope.avatar.push({ value: $scope.user.avatars[i] });
}
}
$scope.avatar = [
{ value: avatarBaseUrl + hash + ".jpg?s=30&d=mm" },
{ value: avatarBaseUrl + hash + ".jpg?s=60&d=mm" },
{ value: avatarBaseUrl + hash + ".jpg?s=90&d=mm" }
];
}
}
}, function errorCallback(response) {
//cannot load it from the server so we cannot do anything
});
}
}));
@@ -143,7 +128,7 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $
//register it
angular.module('umbraco').controller("Umbraco.MainController", MainController).
config(function (tmhDynamicLocaleProvider) {
//Set url for locale files
tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js');
});
config(function (tmhDynamicLocaleProvider) {
//Set url for locale files
tmhDynamicLocaleProvider.localeLocationPattern('lib/angular/1.1.5/i18n/angular-locale_{{locale}}.js');
});

View File

@@ -96,7 +96,7 @@
}
} else if (evt.Message) {
file.serverErrorMessage = evt.Message;
vm.zipFile.serverErrorMessage = evt.Message;
}
}
});

View File

@@ -1,7 +1,7 @@
(function () {
"use strict";
function UserEditController($scope, $timeout, $location, $routeParams, usersResource, contentEditingHelper, localizationService, notificationsService) {
function UserEditController($scope, $timeout, $location, $routeParams, usersResource, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper) {
var vm = this;
var localizeSaving = localizationService.localize("general_saving");
@@ -9,6 +9,7 @@
vm.page = {};
vm.user = {};
vm.breadcrumbs = [];
vm.avatarFile = {};
vm.goToPage = goToPage;
vm.openUserGroupPicker = openUserGroupPicker;
@@ -18,8 +19,10 @@
vm.disableUser = disableUser;
vm.resetPassword = resetPassword;
vm.getUserStateType = getUserStateType;
vm.changeAvatar = changeAvatar;
vm.clearAvatar = clearAvatar;
vm.save = save;
vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"
vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes);
function init() {
@@ -31,7 +34,7 @@
makeBreadcrumbs(vm.user);
vm.loading = false;
});
}
function save() {
@@ -47,7 +50,7 @@
// when server side validation fails - as opposed to content where we are capable of saving the content
// item if server side validation fails
redirectOnFailure: false,
rebindCallback: function (orignal, saved) {}
rebindCallback: function (orignal, saved) { }
}).then(function (saved) {
vm.user = saved;
@@ -72,17 +75,17 @@
selection: vm.user.userGroups,
closeButtonLabel: "Cancel",
show: true,
submit: function(model) {
submit: function (model) {
// apply changes
if(model.selection) {
if (model.selection) {
vm.user.userGroups = model.selection;
}
vm.userGroupPicker.show = false;
vm.userGroupPicker = null;
},
close: function(oldModel) {
close: function (oldModel) {
// rollback on close
if(oldModel.selection) {
if (oldModel.selection) {
vm.user.userGroups = oldModel.selection;
}
vm.userGroupPicker.show = false;
@@ -97,14 +100,14 @@
view: "contentpicker",
multiPicker: true,
show: true,
submit: function(model) {
if(model.selection) {
submit: function (model) {
if (model.selection) {
vm.user.startNodesContent = model.selection;
}
vm.contentPicker.show = false;
vm.contentPicker = null;
},
close: function(oldModel) {
close: function (oldModel) {
vm.contentPicker.show = false;
vm.contentPicker = null;
}
@@ -120,14 +123,14 @@
entityType: "media",
multiPicker: true,
show: true,
submit: function(model) {
if(model.selection) {
submit: function (model) {
if (model.selection) {
vm.user.startNodesMedia = model.selection;
}
vm.contentPicker.show = false;
vm.contentPicker = null;
},
close: function(oldModel) {
close: function (oldModel) {
vm.contentPicker.show = false;
vm.contentPicker = null;
}
@@ -144,6 +147,13 @@
function resetPassword() {
alert("reset password");
}
function clearAvatar() {
// get user
usersResource.clearAvatar(vm.user.id).then(function (data) {
vm.user.avatars = data;
});
}
function getUserStateType(state) {
@@ -157,10 +167,63 @@
}
}
function changeAvatar() {
alert("change avatar");
$scope.changeAvatar = function (files, event) {
if (files && files.length > 0) {
upload(files[0]);
}
};
function upload(file) {
Upload.upload({
url: umbRequestHelper.getApiUrl("userApiBaseUrl", "PostSetAvatar", { id: vm.user.id }),
fields: {},
file: file
}).progress(function (evt) {
//TODO: Do progress, etc...
// set uploading status on file
vm.avatarFile.uploadStatus = "uploading";
}).success(function (data, status, headers, config) {
// set done status on file
vm.avatarFile.uploadStatus = "done";
vm.user.avatars = data;
}).error(function (evt, status, headers, config) {
// set status done
vm.avatarFile.uploadStatus = "error";
// If file not found, server will return a 404 and display this message
if (status === 404) {
vm.avatarFile.serverErrorMessage = "File not found";
}
else if (status == 400) {
//it's a validation error
vm.avatarFile.serverErrorMessage = evt.message;
}
else {
//it's an unhandled error
//if the service returns a detailed error
if (evt.InnerException) {
vm.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage;
//Check if its the common "too large file" exception
if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) {
vm.avatarFile.serverErrorMessage = "File too large to upload";
}
} else if (evt.Message) {
vm.avatarFile.serverErrorMessage = evt.Message;
}
}
});
}
function makeBreadcrumbs() {
vm.breadcrumbs = [
{
@@ -173,7 +236,7 @@
}
];
}
init();
}

View File

@@ -119,18 +119,30 @@
<div class="umb-package-details__section">
<div class="flex flex-column justify-center items-center" style="margin-bottom: 20px;">
<umb-avatar
<ng-form name="avatarForm">
<umb-avatar
color="secondary"
size="xxl"
name="{{vm.user.name}}"
img-src="{{vm.user.avatar}}">
</umb-avatar>
<umb-button
type="button"
button-style="link"
action="vm.changeAvatar()"
label="Change avatar">
</umb-button>
img-src="{{vm.user.avatars[4]}}">
</umb-avatar>
<a
href=""
class="file-select"
ngf-select
ng-model="filesHolder"
ngf-change="changeAvatar($files, $event)"
ngf-multiple="false"
ngf-pattern="{{vm.acceptedFileTypes}}"
ngf-max-size="{{ vm.maxFileSize }}">Change avatar</a>
<br/>
<a href=""
ng-click="vm.clearAvatar()">Clear avatar</a>
</ng-form>
</div>
<!--

View File

@@ -172,7 +172,7 @@
size="l"
color="secondary"
name="{{user.name}}"
img-src="{{user.avatar}}">
img-src="{{user.avatars[2]}}">
</umb-avatar>
</div>
<div class="umb-user__checkmark" ng-if="user.selected"><umb-checkmark checked="user.selected" size="s"></umb-checkmark></div>
@@ -223,7 +223,7 @@
size="xs"
color="secondary"
name="{{user.name}}"
img-src="{{user.avatar}}">
img-src="{{user.avatars[0]}}">
</umb-avatar>
</td>
<td class="bold"><a href="" ng-click="vm.goToUser(user)">{{user.name}}</a></td>

View File

@@ -300,11 +300,7 @@ namespace Umbraco.Web.Editors
{
"logApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl<LogController>(
controller => controller.GetEntityLog(0))
},
{
"gravatarApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl<GravatarController>(
controller => controller.GetCurrentUserGravatarUrl())
},
},
{
"memberApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl<MemberController>(
controller => controller.GetByKey(Guid.Empty))

View File

@@ -1,38 +0,0 @@
using System;
using System.Net;
using Umbraco.Core;
using Umbraco.Web.Mvc;
namespace Umbraco.Web.Editors
{
/// <summary>
/// API controller used for getting Gravatar urls
/// </summary>
[PluginController("UmbracoApi")]
public class GravatarController : UmbracoAuthorizedJsonController
{
public string GetCurrentUserGravatarUrl()
{
var userService = Services.UserService;
var user = userService.GetUserById(UmbracoContext.Security.CurrentUser.Id);
var gravatarHash = user.Email.ToMd5();
var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash;
// Test if we can reach this URL, will fail when there's network or firewall errors
var request = (HttpWebRequest)WebRequest.Create(gravatarUrl);
// Require response within 10 seconds
request.Timeout = 10000;
try
{
using ((HttpWebResponse)request.GetResponse()) { }
}
catch (Exception)
{
// There was an HTTP or other error, return an null instead
return null;
}
return gravatarUrl;
}
}
}

View File

@@ -24,6 +24,8 @@ using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
[UmbracoApplicationAuthorize(Constants.Applications.Users)]
public class UsersController : UmbracoAuthorizedJsonController
@@ -45,8 +47,21 @@ namespace Umbraco.Web.Editors
{
}
/// <summary>
/// Returns a list of the sizes of gravatar urls for the user or null if the gravatar server cannot be reached
/// </summary>
/// <returns></returns>
public string[] GetCurrentUserAvatarUrls()
{
var urls = UmbracoContext.Security.CurrentUser.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache);
if (urls == null)
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Could not access Gravatar endpoint"));
return urls;
}
[FileUploadCleanupFilter(false)]
public async Task<HttpResponseMessage> SetAvatar(int id)
public async Task<HttpResponseMessage> PostSetAvatar(int id)
{
if (Request.Content.IsMimeMultipartContent() == false)
{
@@ -65,14 +80,8 @@ namespace Umbraco.Web.Editors
{
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);
var user = Services.UserService.GetUserById(id);
if (user == null)
return Request.CreateResponse(HttpStatusCode.NotFound);
@@ -89,12 +98,8 @@ namespace Umbraco.Web.Editors
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;
}
//generate a path of known data, we don't want this path to be guessable
user.Avatar = "UserAvatars/" + (user.Id + safeFileName).ToSHA1() + "." + ext;
using (var fs = System.IO.File.OpenRead(file.LocalFileName))
{
@@ -110,14 +115,14 @@ namespace Umbraco.Web.Editors
});
}
return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
return Request.CreateResponse(HttpStatusCode.OK, user.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache));
}
public IHttpActionResult ClearAvatar(int id)
public HttpResponseMessage PostClearAvatar(int id)
{
var found = Services.UserService.GetUserById(id);
if (found == null)
return NotFound();
return Request.CreateResponse(HttpStatusCode.NotFound);
var filePath = found.Avatar;
@@ -128,7 +133,7 @@ namespace Umbraco.Web.Editors
if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath))
FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath);
return Ok();
return Request.CreateResponse(HttpStatusCode.OK, found.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache));
}
/// <summary>

View File

@@ -36,6 +36,12 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "startMediaIds")]
public int[] StartMediaIds { get; set; }
/// <summary>
/// Returns a list of different size avatars
/// </summary>
[DataMember(Name = "avatars")]
public string[] Avatars { get; set; }
/// <summary>
/// A list of sections the user is allowed to view.
/// </summary>

View File

@@ -31,8 +31,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataMember(Name = "lastLoginDate")]
public DateTime LastLoginDate { get; set; }
[DataMember(Name = "customAvatar")]
public string CustomAvatar { get; set; }
/// <summary>
/// Returns a list of different size avatars
/// </summary>
[DataMember(Name = "avatars")]
public string[] Avatars { get; set; }
[DataMember(Name = "userState")]
public UserState UserState { get; set; }

View File

@@ -6,6 +6,7 @@ using Umbraco.Core.Models.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Models.ContentEditing;
using umbraco;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
@@ -88,6 +89,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(detail => detail.AdditionalData, opt => opt.Ignore());
config.CreateMap<IUser, UserDisplay>()
.ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache)))
.ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username))
.ForMember(detail => detail.UserGroups, opt => opt.MapFrom(user => user.Groups))
.ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds))
@@ -110,11 +112,10 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(detail => detail.Trashed, opt => opt.Ignore())
.ForMember(detail => detail.Alias, opt => opt.Ignore())
.ForMember(detail => detail.Trashed, opt => opt.Ignore())
//TODO: Enable this when we can!
.ForMember(detail => detail.CustomAvatar, opt => opt.Ignore())
.ForMember(detail => detail.AdditionalData, opt => opt.Ignore());
config.CreateMap<IUser, UserDetail>()
.ForMember(detail => detail.Avatars, opt => opt.MapFrom(user => user.GetCurrentUserAvatarUrls(applicationContext.Services.UserService, applicationContext.ApplicationCache.RuntimeCache)))
.ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id)))
.ForMember(detail => detail.StartContentIds, opt => opt.MapFrom(user => user.StartContentIds))
.ForMember(detail => detail.StartMediaIds, opt => opt.MapFrom(user => user.StartMediaIds))

View File

@@ -310,7 +310,6 @@
<Compile Include="Editors\EditorValidationResolver.cs" />
<Compile Include="Editors\EditorValidator.cs" />
<Compile Include="Editors\FromJsonPathAttribute.cs" />
<Compile Include="Editors\GravatarController.cs" />
<Compile Include="Editors\TemplateController.cs" />
<Compile Include="Editors\ParameterSwapControllerActionSelector.cs" />
<Compile Include="Editors\CodeFileController.cs" />