445 lines
17 KiB
C#
445 lines
17 KiB
C#
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
|
|
}
|
|
}
|