aboutsummaryrefslogtreecommitdiff
path: root/Timeline/Services
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-10-24 16:56:41 +0800
committer杨宇千 <crupest@outlook.com>2019-10-24 16:56:41 +0800
commit89c106169bd2a16310fdaa6e0c48a3402d97de3a (patch)
treec8f5032f86d8a9e5df0117d438ea741cb0a4f613 /Timeline/Services
parentb67a26248d5dde4c3909c29b92b8a182248bdcc1 (diff)
downloadtimeline-89c106169bd2a16310fdaa6e0c48a3402d97de3a.tar.gz
timeline-89c106169bd2a16310fdaa6e0c48a3402d97de3a.tar.bz2
timeline-89c106169bd2a16310fdaa6e0c48a3402d97de3a.zip
...
Diffstat (limited to 'Timeline/Services')
-rw-r--r--Timeline/Services/AvatarFormatException.cs51
-rw-r--r--Timeline/Services/DatabaseExtensions.cs15
-rw-r--r--Timeline/Services/ETagGenerator.cs17
-rw-r--r--Timeline/Services/UserAvatarService.cs187
-rw-r--r--Timeline/Services/UserService.cs2
5 files changed, 165 insertions, 107 deletions
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
+{
+ /// <summary>
+ /// Thrown when avatar is of bad format.
+ /// </summary>
+ [Serializable]
+ public class AvatarFormatException : Exception
+ {
+ public enum ErrorReason
+ {
+ /// <summary>
+ /// Decoding image failed.
+ /// </summary>
+ CantDecode,
+ /// <summary>
+ /// Decoding succeeded but the real type is not the specified type.
+ /// </summary>
+ UnmatchedFormat,
+ /// <summary>
+ /// Image is not a square.
+ /// </summary>
+ 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
{
/// <summary>
/// Check the existence and get the id of the user.
/// </summary>
/// <param name="username">The username of the user.</param>
/// <returns>The user id.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
- public static async Task<long> CheckAndGetUser(DbSet<User> userDbSet, string username)
+ internal static async Task<long> CheckAndGetUser(DbSet<User> 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
{
+ /// <summary>
+ /// Generate a etag for given source.
+ /// </summary>
+ /// <param name="source">The source data.</param>
+ /// <returns>The generated etag.</returns>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
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,54 +10,25 @@ 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; }
}
/// <summary>
- /// Thrown when avatar is of bad format.
- /// </summary>
- [Serializable]
- public class AvatarDataException : Exception
- {
- public enum ErrorReason
- {
- /// <summary>
- /// Decoding image failed.
- /// </summary>
- CantDecode,
- /// <summary>
- /// Decoding succeeded but the real type is not the specified type.
- /// </summary>
- UnmatchedFormat,
- /// <summary>
- /// Image is not a square.
- /// </summary>
- 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; }
- }
-
- /// <summary>
/// Provider for default user avatar.
/// </summary>
/// <remarks>
@@ -83,7 +54,7 @@ namespace Timeline.Services
/// Validate a avatar's format and size info.
/// </summary>
/// <param name="avatar">The avatar to validate.</param>
- /// <exception cref="AvatarDataException">Thrown when validation failed.</exception>
+ /// <exception cref="AvatarFormatException">Thrown when validation failed.</exception>
Task Validate(Avatar avatar);
}
@@ -94,16 +65,18 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to get avatar etag of.</param>
/// <returns>The etag.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<string> GetAvatarETag(string username);
/// <summary>
- /// 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.
/// </summary>
/// <param name="username">The username of the user to get avatar of.</param>
/// <returns>The avatar info.</returns>
- /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="username"/> is null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
Task<AvatarInfo> GetAvatar(string username);
@@ -112,38 +85,41 @@ namespace Timeline.Services
/// </summary>
/// <param name="username">The username of the user to set avatar for.</param>
/// <param name="avatar">The avatar. Can be null to delete the saved avatar.</param>
- /// <exception cref="ArgumentException">Throw if <paramref name="username"/> is null or empty.
- /// Or thrown if <paramref name="avatar"/> is not null but <see cref="Avatar.Type"/> is null or empty or <see cref="Avatar.Data"/> is null.</exception>
+ /// <exception cref="ArgumentNullException">Throw if <paramref name="username"/> is null.</exception>
+ /// <exception cref="ArgumentException">Thrown if any field in <paramref name="avatar"/> is null when <paramref name="avatar"/> is not null.</exception>
+ /// <exception cref="UsernameBadFormatException">Thrown if the <paramref name="username"/> is of bad format.</exception>
/// <exception cref="UserNotExistException">Thrown if the user does not exist.</exception>
- /// <exception cref="AvatarDataException">Thrown if avatar is of bad format.</exception>
- Task SetAvatar(string username, Avatar avatar);
+ /// <exception cref="AvatarFormatException">Thrown if avatar is of bad format.</exception>
+ 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<string> 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<UserAvatarService> logger,
DatabaseContext database,
@@ -215,13 +191,14 @@ namespace Timeline.Services
_defaultUserAvatarProvider = defaultUserAvatarProvider;
_avatarValidator = avatarValidator;
_eTagGenerator = eTagGenerator;
+ _usernameValidator = new UsernameValidator();
}
public async Task<string> 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<AvatarInfo> 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<IETagGenerator, ETagGenerator>();
services.AddScoped<IUserAvatarService, UserAvatarService>();
services.AddSingleton<IDefaultUserAvatarProvider, DefaultUserAvatarProvider>();
- services.AddSingleton<IUserAvatarValidator, UserAvatarValidator>();
+ services.AddTransient<IUserAvatarValidator, UserAvatarValidator>();
}
}
}
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();