From 89c106169bd2a16310fdaa6e0c48a3402d97de3a Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 24 Oct 2019 16:56:41 +0800 Subject: ... --- Timeline/Controllers/UserAvatarController.cs | 135 +++++++++------ Timeline/Entities/UserAvatar.cs | 1 + Timeline/ErrorCodes.cs | 15 +- Timeline/Filters/ContentHeaderAttributes.cs | 13 +- Timeline/Helpers/LanguageHelper.cs | 12 ++ Timeline/Helpers/Log.cs | 20 --- Timeline/Models/Http/Common.cs | 66 +++++--- .../Controllers/UserAvatarController.Designer.cs | 171 +++++++++++++++++++ .../Controllers/UserAvatarController.en.resx | 144 ++++++++++++++++ .../Controllers/UserAvatarController.resx | 156 +++++++++++++++++ .../Controllers/UserAvatarController.zh.resx | 144 ++++++++++++++++ Timeline/Resources/Models/Http/Common.en.resx | 29 +++- Timeline/Resources/Models/Http/Common.zh.resx | 29 +++- Timeline/Resources/Services/Exception.Designer.cs | 45 +++++ Timeline/Resources/Services/Exception.resx | 15 ++ .../Services/UserAvatarService.Designer.cs | 108 ++++++++++++ Timeline/Resources/Services/UserAvatarService.resx | 135 +++++++++++++++ Timeline/Services/AvatarFormatException.cs | 51 ++++++ Timeline/Services/DatabaseExtensions.cs | 15 +- Timeline/Services/ETagGenerator.cs | 17 +- Timeline/Services/UserAvatarService.cs | 187 ++++++++++----------- Timeline/Services/UserService.cs | 2 +- Timeline/Timeline.csproj | 18 ++ 23 files changed, 1312 insertions(+), 216 deletions(-) create mode 100644 Timeline/Helpers/LanguageHelper.cs create mode 100644 Timeline/Resources/Controllers/UserAvatarController.Designer.cs create mode 100644 Timeline/Resources/Controllers/UserAvatarController.en.resx create mode 100644 Timeline/Resources/Controllers/UserAvatarController.resx create mode 100644 Timeline/Resources/Controllers/UserAvatarController.zh.resx create mode 100644 Timeline/Resources/Services/UserAvatarService.Designer.cs create mode 100644 Timeline/Resources/Services/UserAvatarService.resx create mode 100644 Timeline/Services/AvatarFormatException.cs (limited to 'Timeline') diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 5cba1d93..838a3928 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using System; @@ -8,61 +9,67 @@ using System.Linq; using System.Threading.Tasks; using Timeline.Authentication; using Timeline.Filters; +using Timeline.Helpers; using Timeline.Models.Http; +using Timeline.Models.Validation; using Timeline.Services; -namespace Timeline.Controllers +namespace Timeline { - [ApiController] - public class UserAvatarController : Controller + public static partial class ErrorCodes { - public static class ErrorCodes + public static partial class Http { - public const int Get_UserNotExist = -1001; - - public const int Put_UserNotExist = -2001; - public const int Put_Forbid = -2002; - public const int Put_BadFormat_CantDecode = -2011; - public const int Put_BadFormat_UnmatchedFormat = -2012; - public const int Put_BadFormat_BadSize = -2013; - public const int Put_Content_TooBig = -2021; - public const int Put_Content_UnmatchedLength_Less = -2022; - public const int Put_Content_UnmatchedLength_Bigger = -2023; + public static class UserAvatar // bbb = 003 + { + public static class Get // cc = 01 + { + public const int UserNotExist = 10030101; + } - public const int Delete_UserNotExist = -3001; - public const int Delete_Forbid = -3002; + public static class Put // cc = 02 + { + public const int UserNotExist = 10030201; + public const int Forbid = 10030202; + public const int BadFormat_CantDecode = 10030203; + public const int BadFormat_UnmatchedFormat = 10030204; + public const int BadFormat_BadSize = 10030205; + } - public static int From(AvatarDataException.ErrorReason error) - { - switch (error) + public static class Delete // cc = 03 { - case AvatarDataException.ErrorReason.CantDecode: - return Put_BadFormat_CantDecode; - case AvatarDataException.ErrorReason.UnmatchedFormat: - return Put_BadFormat_UnmatchedFormat; - case AvatarDataException.ErrorReason.BadSize: - return Put_BadFormat_BadSize; - default: - throw new Exception("Unknown AvatarDataException.ErrorReason value."); + public const int UserNotExist = 10030301; + public const int Forbid = 10030302; } } } + } +} +namespace Timeline.Controllers +{ + [ApiController] + public class UserAvatarController : Controller + { private readonly ILogger _logger; private readonly IUserAvatarService _service; - public UserAvatarController(ILogger logger, IUserAvatarService service) + private readonly IStringLocalizerFactory _localizerFactory; + private readonly IStringLocalizer _localizer; + + public UserAvatarController(ILogger logger, IUserAvatarService service, IStringLocalizerFactory localizerFactory) { _logger = logger; _service = service; + _localizerFactory = localizerFactory; + _localizer = new StringLocalizer(localizerFactory); } [HttpGet("users/{username}/avatar")] - [Authorize] [ResponseCache(NoStore = false, Location = ResponseCacheLocation.None, Duration = 0)] - public async Task Get([FromRoute] string username) + public async Task Get([FromRoute][Username] string username) { const string IfNonMatchHeaderKey = "If-None-Match"; @@ -74,11 +81,16 @@ namespace Timeline.Controllers if (Request.Headers.TryGetValue(IfNonMatchHeaderKey, out var value)) { if (!EntityTagHeaderValue.TryParseStrictList(value, out var eTagList)) - return BadRequest(CommonResponse.BadIfNonMatch()); + { + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetBadIfNoneMatch, + ("Username", username), ("If-None-Match", value))); + return BadRequest(HeaderErrorResponse.BadIfNonMatch(_localizerFactory)); + } if (eTagList.FirstOrDefault(e => e.Equals(eTag)) != null) { Response.Headers.Add("ETag", eTagValue); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnNotModify, ("Username", username))); return StatusCode(StatusCodes.Status304NotModified); } } @@ -86,12 +98,13 @@ namespace Timeline.Controllers var avatarInfo = await _service.GetAvatar(username); var avatar = avatarInfo.Avatar; + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogGetReturnData, ("Username", username))); return File(avatar.Data, avatar.Type, new DateTimeOffset(avatarInfo.LastModified), eTag); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to get a avatar of a non-existent user failed. Username: {username} ."); - return NotFound(new CommonResponse(ErrorCodes.Get_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogGetUserNotExist, ("Username", username))); + return NotFound(new CommonResponse(ErrorCodes.Http.UserAvatar.Get.UserNotExist, _localizer["ErrorGetUserNotExist"])); } } @@ -99,18 +112,18 @@ namespace Timeline.Controllers [Authorize] [RequireContentType, RequireContentLength] [Consumes("image/png", "image/jpeg", "image/gif", "image/webp")] - public async Task Put(string username) + public async Task Put([FromRoute][Username] string username) { - var contentLength = Request.ContentLength.Value; + var contentLength = Request.ContentLength!.Value; if (contentLength > 1000 * 1000 * 10) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_TooBig, - "Content can't be bigger than 10MB.")); + return BadRequest(ContentErrorResponse.TooBig(_localizerFactory, "10MB")); if (!User.IsAdministrator() && User.Identity.Name != username) { - _logger.LogInformation($"Attempt to put a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutForbid, + ("Operator Username", User.Identity.Name), ("Username To Put Avatar", username))); return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Put_Forbid, "Normal user can't change other's avatar.")); + new CommonResponse(ErrorCodes.Http.UserAvatar.Put.Forbid, _localizer["ErrorPutForbid"])); } try @@ -119,13 +132,11 @@ namespace Timeline.Controllers var bytesRead = await Request.Body.ReadAsync(data); if (bytesRead != contentLength) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Less, - $"Content length in header is {contentLength} but actual length is {bytesRead}.")); + return BadRequest(ContentErrorResponse.UnmatchedLength_Smaller(_localizerFactory)); var extraByte = new byte[1]; if (await Request.Body.ReadAsync(extraByte) != 0) - return BadRequest(new CommonResponse(ErrorCodes.Put_Content_UnmatchedLength_Bigger, - $"Content length in header is {contentLength} but actual length is bigger than that.")); + return BadRequest(ContentErrorResponse.UnmatchedLength_Bigger(_localizerFactory)); await _service.SetAvatar(username, new Avatar { @@ -133,43 +144,57 @@ namespace Timeline.Controllers Type = Request.ContentType }); - _logger.LogInformation($"Succeed to put a avatar of a user. Username: {username} ; Mime Type: {Request.ContentType} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutSuccess, + ("Username", username), ("Mime Type", Request.ContentType))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to put a avatar of a non-existent user failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.Put_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserNotExist, ("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Put.UserNotExist, _localizer["ErrorPutUserNotExist"])); } - catch (AvatarDataException e) + catch (AvatarFormatException e) { - _logger.LogInformation(e, $"Attempt to put a avatar of a bad format failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.From(e.Error), "Bad format.")); + var (code, message) = e.Error switch + { + AvatarFormatException.ErrorReason.CantDecode => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_CantDecode, _localizer["ErrorPutBadFormatCantDecode"]), + AvatarFormatException.ErrorReason.UnmatchedFormat => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_UnmatchedFormat, _localizer["ErrorPutBadFormatUnmatchedFormat"]), + AvatarFormatException.ErrorReason.BadSize => + (ErrorCodes.Http.UserAvatar.Put.BadFormat_BadSize, _localizer["ErrorPutBadFormatBadSize"]), + _ => + throw new Exception(Resources.Controllers.UserAvatarController.ExceptionUnknownAvatarFormatError) + }; + + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, ("Username", username))); + return BadRequest(new CommonResponse(code, message)); } } [HttpDelete("users/{username}/avatar")] [Authorize] - public async Task Delete([FromRoute] string username) + public async Task Delete([FromRoute][Username] string username) { if (!User.IsAdministrator() && User.Identity.Name != username) { - _logger.LogInformation($"Attempt to delete a avatar of other user as a non-admin failed. Operator Username: {User.Identity.Name} ; Username To Put Avatar: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogPutUserBadFormat, + ("Operator Username", User.Identity.Name), ("Username To Delete Avatar", username))); return StatusCode(StatusCodes.Status403Forbidden, - new CommonResponse(ErrorCodes.Delete_Forbid, "Normal user can't delete other's avatar.")); + new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.Forbid, _localizer["ErrorDeleteForbid"])); } try { await _service.SetAvatar(username, null); - _logger.LogInformation($"Succeed to delete a avatar of a user. Username: {username} ."); + _logger.LogInformation(Log.Format(Resources.Controllers.UserAvatarController.LogDeleteSuccess, ("Username", username))); return Ok(); } catch (UserNotExistException e) { - _logger.LogInformation(e, $"Attempt to delete a avatar of a non-existent user failed. Username: {username} ."); - return BadRequest(new CommonResponse(ErrorCodes.Delete_UserNotExist, "User does not exist.")); + _logger.LogInformation(e, Log.Format(Resources.Controllers.UserAvatarController.LogDeleteNotExist, ("Username", username))); + return BadRequest(new CommonResponse(ErrorCodes.Http.UserAvatar.Delete.UserNotExist, _localizer["ErrorDeleteUserNotExist"])); } } } diff --git a/Timeline/Entities/UserAvatar.cs b/Timeline/Entities/UserAvatar.cs index d47bb28b..3b5388aa 100644 --- a/Timeline/Entities/UserAvatar.cs +++ b/Timeline/Entities/UserAvatar.cs @@ -11,6 +11,7 @@ namespace Timeline.Entities public long Id { get; set; } [Column("data")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "This is data base entity.")] public byte[]? Data { get; set; } [Column("type")] diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs index 0b325e27..5e7f003a 100644 --- a/Timeline/ErrorCodes.cs +++ b/Timeline/ErrorCodes.cs @@ -17,10 +17,17 @@ public static class Header // cc = 01 { - public const int Missing_ContentType = 10010101; // dd = 01 - public const int Missing_ContentLength = 10010102; // dd = 02 - public const int Zero_ContentLength = 10010103; // dd = 03 - public const int BadFormat_IfNonMatch = 10010104; // dd = 04 + public const int Missing_ContentType = 10000101; // dd = 01 + public const int Missing_ContentLength = 10000102; // dd = 02 + public const int Zero_ContentLength = 10000103; // dd = 03 + public const int BadFormat_IfNonMatch = 10000104; // dd = 04 + } + + public static class Content // cc = 02 + { + public const int TooBig = 1000201; + public const int UnmatchedLength_Smaller = 10030202; + public const int UnmatchedLength_Bigger = 10030203; } } } diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs index 14685a01..e3d4eeb2 100644 --- a/Timeline/Filters/ContentHeaderAttributes.cs +++ b/Timeline/Filters/ContentHeaderAttributes.cs @@ -1,16 +1,20 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; using Timeline.Models.Http; namespace Timeline.Filters { public class RequireContentTypeAttribute : ActionFilterAttribute { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentType == null) { - context.Result = new BadRequestObjectResult(CommonResponse.MissingContentType()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentType(localizerFactory)); } } } @@ -30,17 +34,20 @@ namespace Timeline.Filters public bool RequireNonZero { get; set; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] public override void OnActionExecuting(ActionExecutingContext context) { if (context.HttpContext.Request.ContentLength == null) { - context.Result = new BadRequestObjectResult(CommonResponse.MissingContentLength()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentLength(localizerFactory)); return; } if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) { - context.Result = new BadRequestObjectResult(CommonResponse.ZeroContentLength()); + var localizerFactory = context.HttpContext.RequestServices.GetRequiredService(); + context.Result = new BadRequestObjectResult(HeaderErrorResponse.ZeroContentLength(localizerFactory)); return; } } diff --git a/Timeline/Helpers/LanguageHelper.cs b/Timeline/Helpers/LanguageHelper.cs new file mode 100644 index 00000000..b0156b8b --- /dev/null +++ b/Timeline/Helpers/LanguageHelper.cs @@ -0,0 +1,12 @@ +using System.Linq; + +namespace Timeline.Helpers +{ + public static class LanguageHelper + { + public static bool AreSame(this bool firstBool, params bool[] otherBools) + { + return otherBools.All(b => b == firstBool); + } + } +} diff --git a/Timeline/Helpers/Log.cs b/Timeline/Helpers/Log.cs index 8deebf1d..68c975fa 100644 --- a/Timeline/Helpers/Log.cs +++ b/Timeline/Helpers/Log.cs @@ -3,26 +3,6 @@ using System.Text; namespace Timeline.Helpers { - // TODO! Remember to remove this after refactor. - public static class MyLogHelper - { - public static KeyValuePair Pair(string key, object value) => new KeyValuePair(key, value); - - public static string FormatLogMessage(string summary, params KeyValuePair[] properties) - { - var builder = new StringBuilder(); - builder.Append(summary); - foreach (var property in properties) - { - builder.AppendLine(); - builder.Append(property.Key); - builder.Append(" : "); - builder.Append(property.Value); - } - return builder.ToString(); - } - } - public static class Log { public static string Format(string summary, params (string, object?)[] properties) diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index c741837a..39ddddd9 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -5,46 +5,74 @@ namespace Timeline.Models.Http { public class CommonResponse { - public static CommonResponse InvalidModel(string message) + internal static CommonResponse InvalidModel(string message) { return new CommonResponse(ErrorCodes.Http.Common.InvalidModel, message); } - public static CommonResponse MissingContentType() + public CommonResponse() { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, "Header Content-Type is required."); + } - public static CommonResponse MissingContentLength() + public CommonResponse(int code, string message) { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, "Header Content-Length is missing or of bad format."); + Code = code; + Message = message; } - public static CommonResponse ZeroContentLength() + public int Code { get; set; } + public string? Message { get; set; } + } + + internal static class HeaderErrorResponse + { + internal static CommonResponse MissingContentType(IStringLocalizerFactory localizerFactory) { - return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, "Header Content-Length must not be 0."); + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, localizer["HeaderMissingContentType"]); } - public static CommonResponse BadIfNonMatch() + internal static CommonResponse MissingContentLength(IStringLocalizerFactory localizerFactory) { - return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, "Header If-Non-Match is of bad format."); + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, localizer["HeaderMissingContentLength"]); } - public CommonResponse() + internal static CommonResponse ZeroContentLength(IStringLocalizerFactory localizerFactory) { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, localizer["HeaderZeroContentLength"]); + } + internal static CommonResponse BadIfNonMatch(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, localizer["HeaderBadIfNonMatch"]); } + } - public CommonResponse(int code, string message) + internal static class ContentErrorResponse + { + internal static CommonResponse TooBig(IStringLocalizerFactory localizerFactory, string maxLength) { - Code = code; - Message = message; + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.TooBig, localizer["ContentTooBig", maxLength]); } - public int Code { get; set; } - public string? Message { get; set; } + internal static CommonResponse UnmatchedLength_Smaller(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Smaller, localizer["ContentUnmatchedLengthSmaller"]); + } + internal static CommonResponse UnmatchedLength_Bigger(IStringLocalizerFactory localizerFactory) + { + var localizer = localizerFactory.Create("Models.Http.Common"); + return new CommonResponse(ErrorCodes.Http.Common.Content.UnmatchedLength_Bigger, localizer["ContentUnmatchedLengthBigger"]); + } } + public class CommonDataResponse : CommonResponse { public CommonDataResponse() @@ -87,13 +115,13 @@ namespace Timeline.Models.Http internal static CommonPutResponse Create(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonPutResponse(0, localizer["ResponsePutCreate"], true); + return new CommonPutResponse(0, localizer["PutCreate"], true); } internal static CommonPutResponse Modify(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonPutResponse(0, localizer["ResponsePutModify"], false); + return new CommonPutResponse(0, localizer["PutModify"], false); } } @@ -124,13 +152,13 @@ namespace Timeline.Models.Http internal static CommonDeleteResponse Delete(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Http.Common"); - return new CommonDeleteResponse(0, localizer["ResponseDeleteDelete"], true); + return new CommonDeleteResponse(0, localizer["DeleteDelete"], true); } internal static CommonDeleteResponse NotExist(IStringLocalizerFactory localizerFactory) { var localizer = localizerFactory.Create("Models.Models.Http.Common"); - return new CommonDeleteResponse(0, localizer["ResponseDeleteNotExist"], false); + return new CommonDeleteResponse(0, localizer["DeleteNotExist"], false); } } } diff --git a/Timeline/Resources/Controllers/UserAvatarController.Designer.cs b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs new file mode 100644 index 00000000..e6eeb1e8 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.Designer.cs @@ -0,0 +1,171 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Controllers { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserAvatarController { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarController() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Controllers.UserAvatarController", typeof(UserAvatarController).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unknown AvatarDataException.ErrorReason value.. + /// + internal static string ExceptionUnknownAvatarFormatError { + get { + return ResourceManager.GetString("ExceptionUnknownAvatarFormatError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of other user as a non-admin failed.. + /// + internal static string LogDeleteForbid { + get { + return ResourceManager.GetString("LogDeleteForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to delete a avatar of a non-existent user failed.. + /// + internal static string LogDeleteNotExist { + get { + return ResourceManager.GetString("LogDeleteNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to delete a avatar of a user.. + /// + internal static string LogDeleteSuccess { + get { + return ResourceManager.GetString("LogDeleteSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to get a avatar with If-None-Match in bad format.. + /// + internal static string LogGetBadIfNoneMatch { + get { + return ResourceManager.GetString("LogGetBadIfNoneMatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returned full data for a get avatar attempt.. + /// + internal static string LogGetReturnData { + get { + return ResourceManager.GetString("LogGetReturnData", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returned NotModify for a get avatar attempt.. + /// + internal static string LogGetReturnNotModify { + get { + return ResourceManager.GetString("LogGetReturnNotModify", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to get a avatar of a non-existent user failed.. + /// + internal static string LogGetUserNotExist { + get { + return ResourceManager.GetString("LogGetUserNotExist", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of other user as a non-admin failed.. + /// + internal static string LogPutForbid { + get { + return ResourceManager.GetString("LogPutForbid", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Succeed to put a avatar of a user.. + /// + internal static string LogPutSuccess { + get { + return ResourceManager.GetString("LogPutSuccess", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a bad format failed.. + /// + internal static string LogPutUserBadFormat { + get { + return ResourceManager.GetString("LogPutUserBadFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attempt to put a avatar of a non-existent user failed.. + /// + internal static string LogPutUserNotExist { + get { + return ResourceManager.GetString("LogPutUserNotExist", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Controllers/UserAvatarController.en.resx b/Timeline/Resources/Controllers/UserAvatarController.en.resx new file mode 100644 index 00000000..cf92ae6d --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.en.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Normal user can't delete other's avatar. + + + User does not exist. + + + User does not exist. + + + Image is not a square. + + + Decoding image failed. + + + Image format is not the one in header. + + + Normal user can't change other's avatar. + + + User does not exist. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.resx b/Timeline/Resources/Controllers/UserAvatarController.resx new file mode 100644 index 00000000..58860c83 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.resx @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unknown AvatarDataException.ErrorReason value. + + + Attempt to delete a avatar of other user as a non-admin failed. + + + Attempt to delete a avatar of a non-existent user failed. + + + Succeed to delete a avatar of a user. + + + Attempt to get a avatar with If-None-Match in bad format. + + + Returned full data for a get avatar attempt. + + + Returned NotModify for a get avatar attempt. + + + Attempt to get a avatar of a non-existent user failed. + + + Attempt to put a avatar of other user as a non-admin failed. + + + Succeed to put a avatar of a user. + + + Attempt to put a avatar of a bad format failed. + + + Attempt to put a avatar of a non-existent user failed. + + \ No newline at end of file diff --git a/Timeline/Resources/Controllers/UserAvatarController.zh.resx b/Timeline/Resources/Controllers/UserAvatarController.zh.resx new file mode 100644 index 00000000..94de1606 --- /dev/null +++ b/Timeline/Resources/Controllers/UserAvatarController.zh.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 普通用户不能删除其他用户的头像。 + + + 用户不存在。 + + + 用户不存在。 + + + 图片不是正方形。 + + + 解码图片失败。 + + + 图片格式与请求头中指示的不一样。 + + + 普通用户不能修改其他用户的头像。 + + + 用户不存在。 + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.en.resx b/Timeline/Resources/Models/Http/Common.en.resx index 40d44191..10407d76 100644 --- a/Timeline/Resources/Models/Http/Common.en.resx +++ b/Timeline/Resources/Models/Http/Common.en.resx @@ -117,16 +117,37 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + Body is too big. It can't be bigger than {0}. + + + Actual body length is bigger than it in header. + + + Actual body length is smaller than it in header. + + An existent item is deleted. - + The item does not exist, so nothing is changed. - + + Header If-Non-Match is of bad format. + + + Header Content-Length is missing or of bad format. + + + Header Content-Type is required. + + + Header Content-Length must not be 0. + + A new item is created. - + An existent item is modified. \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx index b6d955d9..528dc7ab 100644 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -117,16 +117,37 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + 请求体太大。它不能超过{0}. + + + 实际的请求体长度比头中指示的大。 + + + 实际的请求体长度比头中指示的小。 + + 删除了一个项目。 - + 要删除的项目不存在,什么都没有修改。 - + + 头If-Non-Match格式不对。 + + + 头Content-Length缺失或者格式不对。 + + + 缺少必需的头Content-Type。 + + + 头Content-Length不能为0。 + + 创建了一个新项目。 - + 修改了一个已存在的项目。 \ No newline at end of file diff --git a/Timeline/Resources/Services/Exception.Designer.cs b/Timeline/Resources/Services/Exception.Designer.cs index 24f6b8e6..ddf60f45 100644 --- a/Timeline/Resources/Services/Exception.Designer.cs +++ b/Timeline/Resources/Services/Exception.Designer.cs @@ -60,6 +60,51 @@ namespace Timeline.Resources.Services { } } + /// + /// Looks up a localized string similar to Avartar is of bad format because {0}.. + /// + internal static string AvatarFormatException { + get { + return ResourceManager.GetString("AvatarFormatException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image is not a square, aka, width is not equal to height. + /// + internal static string AvatarFormatExceptionBadSize { + get { + return ResourceManager.GetString("AvatarFormatExceptionBadSize", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to failed to decode image, see inner exception. + /// + internal static string AvatarFormatExceptionCantDecode { + get { + return ResourceManager.GetString("AvatarFormatExceptionCantDecode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to unknown error. + /// + internal static string AvatarFormatExceptionUnknownError { + get { + return ResourceManager.GetString("AvatarFormatExceptionUnknownError", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to image's actual mime type is not the specified one. + /// + internal static string AvatarFormatExceptionUnmatchedFormat { + get { + return ResourceManager.GetString("AvatarFormatExceptionUnmatchedFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The password is wrong.. /// diff --git a/Timeline/Resources/Services/Exception.resx b/Timeline/Resources/Services/Exception.resx index 408c45a1..12bf9afb 100644 --- a/Timeline/Resources/Services/Exception.resx +++ b/Timeline/Resources/Services/Exception.resx @@ -117,6 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Avartar is of bad format because {0}. + + + image is not a square, aka, width is not equal to height + + + failed to decode image, see inner exception + + + unknown error + + + image's actual mime type is not the specified one + The password is wrong. diff --git a/Timeline/Resources/Services/UserAvatarService.Designer.cs b/Timeline/Resources/Services/UserAvatarService.Designer.cs new file mode 100644 index 00000000..cabc9ede --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserAvatarService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserAvatarService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserAvatarService", typeof(UserAvatarService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Data of avatar is null.. + /// + internal static string ArgumentAvatarDataNull { + get { + return ResourceManager.GetString("ArgumentAvatarDataNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type of avatar is null.. + /// + internal static string ArgumentAvatarTypeNull { + get { + return ResourceManager.GetString("ArgumentAvatarTypeNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Database corupted! One of type and data of a avatar is null but the other is not.. + /// + internal static string DatabaseCorruptedDataAndTypeNotSame { + get { + return ResourceManager.GetString("DatabaseCorruptedDataAndTypeNotSame", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Created an entry in user_avatars.. + /// + internal static string LogCreateEntity { + get { + return ResourceManager.GetString("LogCreateEntity", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated an entry in user_avatars.. + /// + internal static string LogUpdateEntity { + get { + return ResourceManager.GetString("LogUpdateEntity", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserAvatarService.resx b/Timeline/Resources/Services/UserAvatarService.resx new file mode 100644 index 00000000..ab6389ff --- /dev/null +++ b/Timeline/Resources/Services/UserAvatarService.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Data of avatar is null. + + + Type of avatar is null. + + + Database corupted! One of type and data of a avatar is null but the other is not. + + + Created an entry in user_avatars. + + + Updated an entry in user_avatars. + + \ No newline at end of file diff --git a/Timeline/Services/AvatarFormatException.cs b/Timeline/Services/AvatarFormatException.cs new file mode 100644 index 00000000..788eabb2 --- /dev/null +++ b/Timeline/Services/AvatarFormatException.cs @@ -0,0 +1,51 @@ +using System; +using System.Globalization; + +namespace Timeline.Services +{ + /// + /// Thrown when avatar is of bad format. + /// + [Serializable] + public class AvatarFormatException : Exception + { + public enum ErrorReason + { + /// + /// Decoding image failed. + /// + CantDecode, + /// + /// Decoding succeeded but the real type is not the specified type. + /// + UnmatchedFormat, + /// + /// Image is not a square. + /// + BadSize + } + + public AvatarFormatException() : base(MakeMessage(null)) { } + public AvatarFormatException(string message) : base(message) { } + public AvatarFormatException(string message, Exception inner) : base(message, inner) { } + + public AvatarFormatException(Avatar avatar, ErrorReason error) : base(MakeMessage(error)) { Avatar = avatar; Error = error; } + public AvatarFormatException(Avatar avatar, ErrorReason error, Exception inner) : base(MakeMessage(error), inner) { Avatar = avatar; Error = error; } + + protected AvatarFormatException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + private static string MakeMessage(ErrorReason? reason) => + string.Format(CultureInfo.InvariantCulture, Resources.Services.Exception.AvatarFormatException, reason switch + { + ErrorReason.CantDecode => Resources.Services.Exception.AvatarFormatExceptionCantDecode, + ErrorReason.UnmatchedFormat => Resources.Services.Exception.AvatarFormatExceptionUnmatchedFormat, + ErrorReason.BadSize => Resources.Services.Exception.AvatarFormatExceptionBadSize, + _ => Resources.Services.Exception.AvatarFormatExceptionUnknownError + }); + + public ErrorReason? Error { get; set; } + public Avatar? Avatar { get; set; } + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index a37cf05b..62b22f00 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -4,22 +4,27 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models.Validation; namespace Timeline.Services { - public static class DatabaseExtensions + internal static class DatabaseExtensions { /// /// Check the existence and get the id of the user. /// /// The username of the user. /// The user id. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if is of bad format. /// Thrown if user does not exist. - public static async Task CheckAndGetUser(DbSet userDbSet, string username) + internal static async Task CheckAndGetUser(DbSet userDbSet, UsernameValidator validator, string username) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); + if (username == null) + throw new ArgumentNullException(nameof(username)); + var (result, messageGenerator) = validator.Validate(username); + if (!result) + throw new UsernameBadFormatException(username, messageGenerator(null)); var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); if (userId == 0) diff --git a/Timeline/Services/ETagGenerator.cs b/Timeline/Services/ETagGenerator.cs index e2abebdc..e518f01f 100644 --- a/Timeline/Services/ETagGenerator.cs +++ b/Timeline/Services/ETagGenerator.cs @@ -5,13 +5,20 @@ namespace Timeline.Services { public interface IETagGenerator { + /// + /// Generate a etag for given source. + /// + /// The source data. + /// The generated etag. + /// Thrown if is null. string Generate(byte[] source); } - public class ETagGenerator : IETagGenerator, IDisposable + public sealed class ETagGenerator : IETagGenerator, IDisposable { private readonly SHA1 _sha1; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Security", "CA5350:Do Not Use Weak Cryptographic Algorithms", Justification = "Sha1 is enough ??? I don't know.")] public ETagGenerator() { _sha1 = SHA1.Create(); @@ -19,15 +26,19 @@ namespace Timeline.Services public string Generate(byte[] source) { - if (source == null || source.Length == 0) - throw new ArgumentException("Source is null or empty.", nameof(source)); + if (source == null) + throw new ArgumentNullException(nameof(source)); return Convert.ToBase64String(_sha1.ComputeHash(source)); } + private bool _disposed = false; // To detect redundant calls + public void Dispose() { + if (_disposed) return; _sha1.Dispose(); + _disposed = true; } } } diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index ecec5a31..4c65a0fa 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -10,53 +10,24 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models.Validation; namespace Timeline.Services { public class Avatar { - public string Type { get; set; } - public byte[] Data { get; set; } + public string Type { get; set; } = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "DTO Object")] + public byte[] Data { get; set; } = default!; } public class AvatarInfo { - public Avatar Avatar { get; set; } + public Avatar Avatar { get; set; } = default!; public DateTime LastModified { get; set; } } - /// - /// Thrown when avatar is of bad format. - /// - [Serializable] - public class AvatarDataException : Exception - { - public enum ErrorReason - { - /// - /// Decoding image failed. - /// - CantDecode, - /// - /// Decoding succeeded but the real type is not the specified type. - /// - UnmatchedFormat, - /// - /// Image is not a square. - /// - BadSize - } - - public AvatarDataException(Avatar avatar, ErrorReason error, string message) : base(message) { Avatar = avatar; Error = error; } - public AvatarDataException(Avatar avatar, ErrorReason error, string message, Exception inner) : base(message, inner) { Avatar = avatar; Error = error; } - protected AvatarDataException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - - public ErrorReason Error { get; set; } - public Avatar Avatar { get; set; } - } - /// /// Provider for default user avatar. /// @@ -83,7 +54,7 @@ namespace Timeline.Services /// Validate a avatar's format and size info. /// /// The avatar to validate. - /// Thrown when validation failed. + /// Thrown when validation failed. Task Validate(Avatar avatar); } @@ -94,16 +65,18 @@ namespace Timeline.Services /// /// The username of the user to get avatar etag of. /// The etag. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatarETag(string username); /// - /// Get avatar of a user. If the user has no avatar, a default one is returned. + /// Get avatar of a user. If the user has no avatar set, a default one is returned. /// /// The username of the user to get avatar of. /// The avatar info. - /// Thrown if is null or empty. + /// Thrown if is null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. Task GetAvatar(string username); @@ -112,38 +85,41 @@ namespace Timeline.Services /// /// The username of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. - /// Throw if is null or empty. - /// Or thrown if is not null but is null or empty or is null. + /// Throw if is null. + /// Thrown if any field in is null when is not null. + /// Thrown if the is of bad format. /// Thrown if the user does not exist. - /// Thrown if avatar is of bad format. - Task SetAvatar(string username, Avatar avatar); + /// Thrown if avatar is of bad format. + Task SetAvatar(string username, Avatar? avatar); } + // TODO! : Make this configurable. public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider { - private readonly IWebHostEnvironment _environment; - private readonly IETagGenerator _eTagGenerator; - private byte[] _cacheData; + private readonly string _avatarPath; + + private byte[] _cacheData = default!; private DateTime _cacheLastModified; - private string _cacheETag; + private string _cacheETag = default!; + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "DI.")] public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator) { - _environment = environment; + _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png"); _eTagGenerator = eTagGenerator; } private async Task CheckAndInit() { - if (_cacheData != null) - return; - - var path = Path.Combine(_environment.ContentRootPath, "default-avatar.png"); - _cacheData = await File.ReadAllBytesAsync(path); - _cacheLastModified = File.GetLastWriteTime(path); - _cacheETag = _eTagGenerator.Generate(_cacheData); + var path = _avatarPath; + if (_cacheData == null || File.GetLastWriteTime(path) > _cacheLastModified) + { + _cacheData = await File.ReadAllBytesAsync(path); + _cacheLastModified = File.GetLastWriteTime(path); + _cacheETag = _eTagGenerator.Generate(_cacheData); + } } public async Task GetDefaultAvatarETag() @@ -175,17 +151,15 @@ namespace Timeline.Services { try { - using (var image = Image.Load(avatar.Data, out IImageFormat format)) - { - if (!format.MimeTypes.Contains(avatar.Type)) - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.UnmatchedFormat, "Image's actual mime type is not the specified one."); - if (image.Width != image.Height) - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.BadSize, "Image is not a square, aka, width is not equal to height."); - } + using var image = Image.Load(avatar.Data, out IImageFormat format); + if (!format.MimeTypes.Contains(avatar.Type)) + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.UnmatchedFormat); + if (image.Width != image.Height) + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.BadSize); } catch (UnknownImageFormatException e) { - throw new AvatarDataException(avatar, AvatarDataException.ErrorReason.CantDecode, "Failed to decode image. See inner exception.", e); + throw new AvatarFormatException(avatar, AvatarFormatException.ErrorReason.CantDecode, e); } }); } @@ -203,6 +177,8 @@ namespace Timeline.Services private readonly IETagGenerator _eTagGenerator; + private readonly UsernameValidator _usernameValidator; + public UserAvatarService( ILogger logger, DatabaseContext database, @@ -215,13 +191,14 @@ namespace Timeline.Services _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; _eTagGenerator = eTagGenerator; + _usernameValidator = new UsernameValidator(); } public async Task GetAvatarETag(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; + var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); else @@ -230,54 +207,57 @@ namespace Timeline.Services public async Task GetAvatar(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); - var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); - if ((avatar.Type == null) != (avatar.Data == null)) + if (avatarEntity != null) { - _logger.LogCritical("Database corupted! One of type and data of a avatar is null but the other is not."); - throw new DatabaseCorruptedException(); - } + if (!LanguageHelper.AreSame(avatarEntity.Data == null, avatarEntity.Type == null)) + { + var message = Resources.Services.UserAvatarService.DatabaseCorruptedDataAndTypeNotSame; + _logger.LogCritical(message); + throw new DatabaseCorruptedException(message); + } - if (avatar.Data == null) - { - var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); - defaultAvatar.LastModified = defaultAvatar.LastModified > avatar.LastModified ? defaultAvatar.LastModified : avatar.LastModified; - return defaultAvatar; - } - else - { - return new AvatarInfo + if (avatarEntity.Data != null) { - Avatar = new Avatar + return new AvatarInfo { - Type = avatar.Type, - Data = avatar.Data - }, - LastModified = avatar.LastModified - }; + Avatar = new Avatar + { + Type = avatarEntity.Type!, + Data = avatarEntity.Data + }, + LastModified = avatarEntity.LastModified + }; + } } + var defaultAvatar = await _defaultUserAvatarProvider.GetDefaultAvatar(); + if (avatarEntity != null) + defaultAvatar.LastModified = defaultAvatar.LastModified > avatarEntity.LastModified ? defaultAvatar.LastModified : avatarEntity.LastModified; + return defaultAvatar; } - public async Task SetAvatar(string username, Avatar avatar) + public async Task SetAvatar(string username, Avatar? avatar) { if (avatar != null) { - if (string.IsNullOrEmpty(avatar.Type)) - throw new ArgumentException("Type of avatar is null or empty.", nameof(avatar)); if (avatar.Data == null) - throw new ArgumentException("Data of avatar is null.", nameof(avatar)); + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarDataNull, nameof(avatar)); + if (avatar.Type == null) + throw new ArgumentException(Resources.Services.UserAvatarService.ArgumentAvatarTypeNull, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); - - var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); if (avatar == null) { - if (avatarEntity.Data == null) + if (avatarEntity == null || avatarEntity.Data == null) + { return; + } else { avatarEntity.Data = null; @@ -285,18 +265,29 @@ namespace Timeline.Services avatarEntity.ETag = null; avatarEntity.LastModified = DateTime.Now; await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } } else { await _avatarValidator.Validate(avatar); - avatarEntity.Type = avatar.Type; + var create = avatarEntity == null; + if (create) + { + avatarEntity = new UserAvatar(); + } + avatarEntity!.Type = avatar.Type; avatarEntity.Data = avatar.Data; avatarEntity.ETag = _eTagGenerator.Generate(avatar.Data); avatarEntity.LastModified = DateTime.Now; + if (create) + { + _database.UserAvatars.Add(avatarEntity); + } await _database.SaveChangesAsync(); - _logger.LogInformation("Updated an entry in user_avatars."); + _logger.LogInformation(create ? + Resources.Services.UserAvatarService.LogCreateEntity + : Resources.Services.UserAvatarService.LogUpdateEntity); } } } @@ -308,7 +299,7 @@ namespace Timeline.Services services.TryAddTransient(); services.AddScoped(); services.AddSingleton(); - services.AddSingleton(); + services.AddTransient(); } } } diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index d706d05e..f1317856 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -272,7 +272,7 @@ namespace Timeline.Services Name = username, EncryptedPassword = _passwordService.HashPassword(password), RoleString = UserRoleConvert.ToString(administrator), - Avatar = UserAvatar.Create(DateTime.Now) + Avatar = null }; await _databaseContext.AddAsync(newUser); await _databaseContext.SaveChangesAsync(); diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 0ba34471..519a802d 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -49,6 +49,11 @@ True TokenController.resx + + True + True + UserAvatarController.resx + True True @@ -69,6 +74,11 @@ True Exception.resx + + True + True + UserAvatarService.resx + True True @@ -97,6 +107,10 @@ + + ResXFileCodeGenerator + UserAvatarController.Designer.cs + ResXFileCodeGenerator UserController.Designer.cs @@ -116,6 +130,10 @@ ResXFileCodeGenerator Exception.Designer.cs + + ResXFileCodeGenerator + UserAvatarService.Designer.cs + ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3