添加项目文件。
This commit is contained in:
444
TightWiki/Controllers/ProfileController.cs
Normal file
444
TightWiki/Controllers/ProfileController.cs
Normal file
@@ -0,0 +1,444 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using SixLabors.ImageSharp;
|
||||
using System.Security.Claims;
|
||||
using TightWiki.Caching;
|
||||
using TightWiki.Engine;
|
||||
using TightWiki.Engine.Implementation.Utility;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Models.DataModels;
|
||||
using TightWiki.Models.ViewModels.Profile;
|
||||
using TightWiki.Repository;
|
||||
using static TightWiki.Library.Images;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Route("[controller]")]
|
||||
public class ProfileController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IWebHostEnvironment environment)
|
||||
: WikiControllerBase(signInManager, userManager)
|
||||
{
|
||||
private readonly IWebHostEnvironment _environment = environment;
|
||||
|
||||
#region User Profile.
|
||||
|
||||
/// <summary>
|
||||
/// //Gets a users avatar.
|
||||
/// </summary>
|
||||
/// <param name="navigation"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet]
|
||||
[HttpGet("{userAccountName}/Avatar")]
|
||||
public ActionResult Avatar(string userAccountName)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
SessionState.Page.Name = $"Avatar";
|
||||
|
||||
string givenScale = Request.Query["Scale"].ToString().ToString().DefaultWhenNullOrEmpty("100");
|
||||
string givenMax = Request.Query["max"].ToString().DefaultWhenNullOrEmpty("512");
|
||||
string? givenExact = Request.Query["exact"];
|
||||
|
||||
ProfileAvatar? avatar;
|
||||
if (GlobalConfiguration.EnablePublicProfiles)
|
||||
{
|
||||
avatar = UsersRepository.GetProfileAvatarByNavigation(NamespaceNavigation.CleanAndValidate(userAccountName));
|
||||
}
|
||||
else
|
||||
{
|
||||
avatar = new ProfileAvatar();
|
||||
}
|
||||
|
||||
if (avatar.Bytes == null || avatar.Bytes.Length == 0)
|
||||
{
|
||||
//Load the default avatar.
|
||||
var filePath = Path.Combine(_environment.WebRootPath, "Avatar.png");
|
||||
var image = Image.Load(filePath);
|
||||
using var ms = new MemoryStream();
|
||||
image.SaveAsPng(ms);
|
||||
avatar.ContentType = "image/png";
|
||||
avatar.Bytes = ms.ToArray();
|
||||
}
|
||||
|
||||
if (avatar.Bytes != null && avatar.Bytes.Length > 0)
|
||||
{
|
||||
if (avatar.ContentType == "image/x-icon")
|
||||
{
|
||||
//We do not handle the resizing of icon file. Maybe later....
|
||||
return File(avatar.Bytes, avatar.ContentType);
|
||||
}
|
||||
|
||||
var img = Image.Load(new MemoryStream(avatar.Bytes));
|
||||
|
||||
int width = img.Width;
|
||||
int height = img.Height;
|
||||
|
||||
int parsedScale = int.Parse(givenScale);
|
||||
int parsedMax = int.Parse(givenMax);
|
||||
|
||||
if (string.IsNullOrEmpty(givenExact) == false)
|
||||
{
|
||||
int parsedExact = int.Parse(givenExact);
|
||||
if (parsedExact > 1024)
|
||||
{
|
||||
parsedExact = 1024;
|
||||
}
|
||||
else if (parsedExact < 16)
|
||||
{
|
||||
parsedExact = 16;
|
||||
}
|
||||
|
||||
int diff = img.Width - parsedExact;
|
||||
width = (int)(img.Width - diff);
|
||||
height = (int)(img.Height - diff);
|
||||
|
||||
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
|
||||
// dimension to become very small (or even negative). So here we will check the height and width
|
||||
// to ensure they are both at least n pixels and adjust both dimensions.
|
||||
if (height < 16)
|
||||
{
|
||||
int difference = 16 - height;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
if (width < 16)
|
||||
{
|
||||
int difference = 16 - width;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
}
|
||||
else if (parsedMax != 0 && (img.Width > parsedMax || img.Height > parsedMax))
|
||||
{
|
||||
int diff = img.Width - parsedMax;
|
||||
width = (int)(img.Width - diff);
|
||||
height = (int)(img.Height - diff);
|
||||
|
||||
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
|
||||
// dimension to become very small (or even negative). So here we will check the height and width
|
||||
// to ensure they are both at least n pixels and adjust both dimensions.
|
||||
if (height < 16)
|
||||
{
|
||||
int difference = 16 - height;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
if (width < 16)
|
||||
{
|
||||
int difference = 16 - width;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
}
|
||||
else if (parsedScale != 100)
|
||||
{
|
||||
width = (int)(img.Width * (parsedScale / 100.0));
|
||||
height = (int)(img.Height * (parsedScale / 100.0));
|
||||
|
||||
//Adjusting by a ratio (and especially after applying additional scaling) may have caused one
|
||||
// dimension to become very small (or even negative). So here we will check the height and width
|
||||
// to ensure they are both at least n pixels and adjust both dimensions.
|
||||
if (height < 16)
|
||||
{
|
||||
int difference = 16 - height;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
if (width < 16)
|
||||
{
|
||||
int difference = 16 - width;
|
||||
height += difference;
|
||||
width += difference;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return File(avatar.Bytes, avatar.ContentType);
|
||||
}
|
||||
|
||||
if (avatar.ContentType.Equals("image/gif", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resized = ResizeGifImage(avatar.Bytes, width, height);
|
||||
return File(resized, "image/gif");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = ResizeImage(img, width, height);
|
||||
using var ms = new MemoryStream();
|
||||
string contentType = BestEffortConvertImage(image, ms, avatar.ContentType);
|
||||
return File(ms.ToArray(), contentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user profile.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{userAccountName}/Public")]
|
||||
public ActionResult Public(string userAccountName)
|
||||
{
|
||||
SessionState.Page.Name = $"Public Profile";
|
||||
|
||||
userAccountName = NamespaceNavigation.CleanAndValidate(userAccountName);
|
||||
|
||||
if (!GlobalConfiguration.EnablePublicProfiles)
|
||||
{
|
||||
return View(new PublicViewModel
|
||||
{
|
||||
ErrorMessage = "Public profiles are disabled."
|
||||
});
|
||||
}
|
||||
|
||||
if (UsersRepository.TryGetAccountProfileByNavigation(userAccountName, out var accountProfile) == false)
|
||||
{
|
||||
return View(new PublicViewModel
|
||||
{
|
||||
ErrorMessage = "The specified user was not found."
|
||||
});
|
||||
}
|
||||
|
||||
var model = new PublicViewModel()
|
||||
{
|
||||
AccountName = accountProfile.AccountName,
|
||||
Navigation = accountProfile.Navigation,
|
||||
Id = accountProfile.UserId,
|
||||
TimeZone = accountProfile.TimeZone,
|
||||
Language = accountProfile.Language,
|
||||
Country = accountProfile.Country,
|
||||
Biography = WikifierLite.Process(accountProfile.Biography),
|
||||
Avatar = accountProfile.Avatar
|
||||
};
|
||||
|
||||
model.RecentlyModified = PageRepository.GetTopRecentlyModifiedPagesInfoByUserId(accountProfile.UserId, GlobalConfiguration.DefaultProfileRecentlyModifiedCount)
|
||||
.OrderByDescending(o => o.ModifiedDate).ThenBy(o => o.Name).ToList();
|
||||
|
||||
foreach (var item in model.RecentlyModified)
|
||||
{
|
||||
var thisRev = PageRepository.GetPageRevisionByNavigation(item.Navigation, item.Revision);
|
||||
var prevRev = PageRepository.GetPageRevisionByNavigation(item.Navigation, item.Revision - 1);
|
||||
item.ChangeSummary = Differentiator.GetComparisonSummary(thisRev?.Body ?? "", prevRev?.Body ?? "");
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user profile.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet]
|
||||
[HttpGet("My")]
|
||||
public ActionResult My()
|
||||
{
|
||||
SessionState.RequireAuthorizedPermission();
|
||||
SessionState.Page.Name = $"My Profile";
|
||||
|
||||
var model = new AccountProfileViewModel()
|
||||
{
|
||||
AccountProfile = AccountProfileAccountViewModel.FromDataModel(
|
||||
UsersRepository.GetAccountProfileByUserId(SessionState.Profile.EnsureNotNull().UserId)),
|
||||
|
||||
Themes = ConfigurationRepository.GetAllThemes(),
|
||||
TimeZones = TimeZoneItem.GetAll(),
|
||||
Countries = CountryItem.GetAll(),
|
||||
Languages = LanguageItem.GetAll()
|
||||
};
|
||||
|
||||
model.AccountProfile.CreatedDate = SessionState.LocalizeDateTime(model.AccountProfile.CreatedDate);
|
||||
model.AccountProfile.ModifiedDate = SessionState.LocalizeDateTime(model.AccountProfile.ModifiedDate);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save user profile.
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("My")]
|
||||
public ActionResult My(AccountProfileViewModel model)
|
||||
{
|
||||
SessionState.RequireAuthorizedPermission();
|
||||
|
||||
SessionState.Page.Name = $"My Profile";
|
||||
|
||||
model.TimeZones = TimeZoneItem.GetAll();
|
||||
model.Countries = CountryItem.GetAll();
|
||||
model.Languages = LanguageItem.GetAll();
|
||||
model.Themes = ConfigurationRepository.GetAllThemes();
|
||||
|
||||
//Get the UserId from the logged in context because we do not trust anything from the model.
|
||||
var userId = SessionState.Profile.EnsureNotNull().UserId;
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = UserManager.FindByIdAsync(userId.ToString()).Result.EnsureNotNull();
|
||||
|
||||
var profile = UsersRepository.GetAccountProfileByUserId(userId);
|
||||
if (!profile.Navigation.Equals(model.AccountProfile.Navigation, StringComparison.CurrentCultureIgnoreCase))
|
||||
{
|
||||
if (UsersRepository.DoesProfileAccountExist(model.AccountProfile.AccountName))
|
||||
{
|
||||
ModelState.AddModelError("Account.AccountName", "Account name is already in use.");
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
model.AccountProfile.Navigation = NamespaceNavigation.CleanAndValidate(model.AccountProfile.AccountName.ToLower());
|
||||
|
||||
var file = Request.Form.Files["Avatar"];
|
||||
if (file != null && file.Length > 0)
|
||||
{
|
||||
if (GlobalConfiguration.AllowableImageTypes.Contains(file.ContentType.ToLower()) == false)
|
||||
{
|
||||
model.ErrorMessage += "Could not save the attached image, type not allowed.\r\n";
|
||||
}
|
||||
else if (file.Length > GlobalConfiguration.MaxAvatarFileSize)
|
||||
{
|
||||
model.ErrorMessage += "Could not save the attached image, too large.\r\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageBytes = Utility.ConvertHttpFileToBytes(file);
|
||||
var image = Image.Load(new MemoryStream(imageBytes));
|
||||
UsersRepository.UpdateProfileAvatar(profile.UserId, imageBytes, file.ContentType.ToLower());
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError("Account.Avatar", "Could not save the attached image.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profile.AccountName = model.AccountProfile.AccountName;
|
||||
profile.Navigation = NamespaceNavigation.CleanAndValidate(model.AccountProfile.AccountName);
|
||||
profile.Biography = model.AccountProfile.Biography;
|
||||
profile.ModifiedDate = DateTime.UtcNow;
|
||||
UsersRepository.UpdateProfile(profile);
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new ("timezone", model.AccountProfile.TimeZone),
|
||||
new (ClaimTypes.Country, model.AccountProfile.Country),
|
||||
new ("language", model.AccountProfile.Language),
|
||||
new ("firstname", model.AccountProfile.FirstName ?? ""),
|
||||
new ("lastname", model.AccountProfile.LastName ?? ""),
|
||||
new ("theme", model.AccountProfile.Theme ?? ""),
|
||||
};
|
||||
SecurityRepository.UpsertUserClaims(UserManager, user, claims);
|
||||
|
||||
SignInManager.RefreshSignInAsync(user);
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.User, [profile.Navigation]));
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.User, [profile.UserId]));
|
||||
|
||||
model.SuccessMessage = "Your profile has been saved.";
|
||||
|
||||
//This is not 100% necessary, I just want to prevent the user from needing to refresh to view the new theme.
|
||||
SessionState.UserTheme = ConfigurationRepository.GetAllThemes().SingleOrDefault(o => o.Name == model.AccountProfile.Theme) ?? GlobalConfiguration.SystemTheme;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete.
|
||||
|
||||
/// <summary>
|
||||
/// User is deleting their own profile.
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("Delete")]
|
||||
public ActionResult Delete(DeleteAccountViewModel model)
|
||||
{
|
||||
SessionState.RequireAuthorizedPermission();
|
||||
|
||||
var profile = UsersRepository.GetBasicProfileByUserId(SessionState.Profile.EnsureNotNull().UserId);
|
||||
|
||||
bool confirmAction = bool.Parse(GetFormValue("IsActionConfirmed").EnsureNotNull());
|
||||
if (confirmAction == true && profile != null)
|
||||
{
|
||||
var user = UserManager.FindByIdAsync(profile.UserId.ToString()).Result;
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound("User not found.");
|
||||
}
|
||||
|
||||
var result = UserManager.DeleteAsync(user).Result;
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new Exception(string.Join("<br />\r\n", result.Errors.Select(o => o.Description)));
|
||||
}
|
||||
|
||||
SignInManager.SignOutAsync();
|
||||
|
||||
UsersRepository.AnonymizeProfile(profile.UserId);
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.User, [profile.Navigation]));
|
||||
|
||||
HttpContext.SignOutAsync(); //Do we still need this??
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Profile/Deleted");
|
||||
}
|
||||
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Profile/My");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User is deleting their own profile.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("Delete")]
|
||||
public ActionResult Delete()
|
||||
{
|
||||
SessionState.RequireAuthorizedPermission();
|
||||
SessionState.Page.Name = $"Delete Account";
|
||||
|
||||
var profile = UsersRepository.GetBasicProfileByUserId(SessionState.Profile.EnsureNotNull().UserId);
|
||||
|
||||
var model = new DeleteAccountViewModel()
|
||||
{
|
||||
AccountName = profile.AccountName
|
||||
};
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
SessionState.Page.Name = $"Delete {profile.AccountName}";
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// User is deleting their own profile.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("Deleted")]
|
||||
public ActionResult Deleted()
|
||||
{
|
||||
var model = new DeletedAccountViewModel()
|
||||
{
|
||||
};
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user