添加项目文件。
This commit is contained in:
115
TightWiki/Controllers/AccountController.cs
Normal file
115
TightWiki/Controllers/AccountController.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using System.Security.Claims;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Models.ViewModels;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Area("Identity")]
|
||||
[Route("Identity/Account")]
|
||||
public class AccountController : WikiControllerBase
|
||||
{
|
||||
private readonly IUserStore<IdentityUser> _userStore;
|
||||
private readonly IUserEmailStore<IdentityUser> _emailStore;
|
||||
|
||||
public AccountController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager, IUserStore<IdentityUser> userStore)
|
||||
: base(signInManager, userManager)
|
||||
{
|
||||
_userStore = userStore;
|
||||
_emailStore = (IUserEmailStore<IdentityUser>)_userStore;
|
||||
}
|
||||
|
||||
[HttpGet("ExternalLogin")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult ExternalLoginHttpGet(string provider, string? returnUrl = null)
|
||||
{
|
||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { area = "Identity", ReturnUrl = returnUrl });
|
||||
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
[HttpPost("ExternalLogin")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult ExternalLoginHttpPost(string provider, string? returnUrl = null)
|
||||
{
|
||||
var redirectUrl = Url.Action(nameof(ExternalLoginCallback), "Account", new { area = "Identity", ReturnUrl = returnUrl });
|
||||
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null, string? remoteError = null)
|
||||
{
|
||||
//We use this model to display any errors that occur.
|
||||
var model = new ExternalLoginCallbackViewModel();
|
||||
|
||||
returnUrl ??= Url.Content("~/");
|
||||
if (remoteError != null)
|
||||
{
|
||||
return NotifyOfError($"Error from external provider: {remoteError}");
|
||||
}
|
||||
|
||||
var info = await SignInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
return NotifyOfError($"Failed to get information from external provider");
|
||||
}
|
||||
|
||||
var user = await UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
|
||||
if (user != null)
|
||||
{
|
||||
// User exists, sign them in:
|
||||
await SignInManager.SignInAsync(user, isPersistent: false);
|
||||
|
||||
if (UsersRepository.TryGetBasicProfileByUserId(Guid.Parse(user.Id), out _) == false)
|
||||
{
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
//User exits but does not have a profile.
|
||||
//This means that the user has authenticated externally, but has yet to complete the signup process.
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Account/ExternalLoginSupplemental", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the user does not exist, check by email
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email).EnsureNotNull();
|
||||
if (string.IsNullOrEmpty(email))
|
||||
{
|
||||
return NotifyOfError($"The email address was not supplied by the external provider.");
|
||||
}
|
||||
|
||||
user = await UserManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
// User with this email exists but not linked with this external login, link them:
|
||||
var result = await UserManager.AddLoginAsync(user, info);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return NotifyOfError(string.Join("<br />\r\n", result.Errors.Select(o => o.Description)));
|
||||
}
|
||||
await SignInManager.SignInAsync(user, isPersistent: false);
|
||||
return LocalRedirect(returnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If user with this email does not exist, then we need to create the user and profile.
|
||||
|
||||
if (GlobalConfiguration.AllowSignup != true)
|
||||
{
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/Identity/Account/RegistrationIsNotAllowed");
|
||||
}
|
||||
|
||||
return RedirectToPage($"{GlobalConfiguration.BasePath}/Account/ExternalLoginSupplemental", new { ReturnUrl = returnUrl });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1804
TightWiki/Controllers/AdminController.cs
Normal file
1804
TightWiki/Controllers/AdminController.cs
Normal file
File diff suppressed because it is too large
Load Diff
481
TightWiki/Controllers/FileController.cs
Normal file
481
TightWiki/Controllers/FileController.cs
Normal file
@@ -0,0 +1,481 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using SixLabors.ImageSharp;
|
||||
using System.Web;
|
||||
using TightWiki.Caching;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Models.DataModels;
|
||||
using TightWiki.Models.ViewModels.File;
|
||||
using TightWiki.Repository;
|
||||
using static TightWiki.Library.Images;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Route("File")]
|
||||
public class FileController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: WikiControllerBase(signInManager, userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets an image attached to a page.
|
||||
/// </summary>
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="fileRevision">The revision of the the file (NOT THE PAGE REVISION).</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("Image/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
|
||||
public ActionResult Image(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
|
||||
{
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
int givenScale = GetQueryValue("Scale", 100);
|
||||
|
||||
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [givenPageNavigation, givenFileNavigation, fileRevision, givenScale]);
|
||||
if (WikiCache.TryGet<ImageCacheItem>(cacheKey, out var cached))
|
||||
{
|
||||
return File(cached.Bytes, cached.ContentType);
|
||||
}
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
|
||||
if (file != null)
|
||||
{
|
||||
if (file.ContentType == "image/x-icon")
|
||||
{
|
||||
//We do not handle the resizing of icon file. Maybe later....
|
||||
return File(file.Data, file.ContentType);
|
||||
}
|
||||
|
||||
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(file.Data));
|
||||
|
||||
if (givenScale > 500)
|
||||
{
|
||||
givenScale = 500;
|
||||
}
|
||||
if (givenScale != 100)
|
||||
{
|
||||
int width = (int)(img.Width * (givenScale / 100.0));
|
||||
int height = (int)(img.Height * (givenScale / 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;
|
||||
}
|
||||
|
||||
if (file.ContentType.Equals("image/gif", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resized = ResizeGifImage(file.Data, width, height);
|
||||
return File(resized, "image/gif");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = ResizeImage(img, width, height);
|
||||
using var ms = new MemoryStream();
|
||||
file.ContentType = BestEffortConvertImage(image, ms, file.ContentType);
|
||||
var cacheItem = new ImageCacheItem(ms.ToArray(), file.ContentType);
|
||||
WikiCache.Put(cacheKey, cacheItem);
|
||||
return File(cacheItem.Bytes, cacheItem.ContentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return File(file.Data, file.ContentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an image from the database, converts it to a PNG with optional scaling and returns it to the client.
|
||||
/// </summary>
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="fileRevision">The revision of the the FILE (NOT THE PAGE REVISION)</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Png/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
|
||||
public ActionResult Png(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
int givenScale = GetQueryValue("Scale", 100);
|
||||
|
||||
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [givenPageNavigation, givenFileNavigation, fileRevision, givenScale]);
|
||||
if (WikiCache.TryGet<ImageCacheItem>(cacheKey, out var cached))
|
||||
{
|
||||
return File(cached.Bytes, cached.ContentType);
|
||||
}
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
|
||||
if (file != null)
|
||||
{
|
||||
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(Utility.Decompress(file.Data)));
|
||||
|
||||
if (givenScale > 500)
|
||||
{
|
||||
givenScale = 500;
|
||||
}
|
||||
|
||||
if (givenScale != 100)
|
||||
{
|
||||
int width = (int)(img.Width * (givenScale / 100.0));
|
||||
int height = (int)(img.Height * (givenScale / 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;
|
||||
}
|
||||
|
||||
using var image = Images.ResizeImage(img, width, height);
|
||||
using var ms = new MemoryStream();
|
||||
image.SaveAsPng(ms);
|
||||
|
||||
var cacheItem = new ImageCacheItem(ms.ToArray(), "image/png");
|
||||
WikiCache.Put(cacheKey, cacheItem);
|
||||
return File(cacheItem.Bytes, cacheItem.ContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
img.SaveAsPng(ms);
|
||||
|
||||
var cacheItem = new ImageCacheItem(ms.ToArray(), "image/png");
|
||||
WikiCache.Put(cacheKey, cacheItem);
|
||||
return File(cacheItem.Bytes, cacheItem.ContentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file from the database and returns it to the client. This works even
|
||||
/// when the file or file revision is not even attached to the page anymore.
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="fileRevision">The revision of the the FILE (NOT THE PAGE REVISION),</param>
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Binary/{givenPageNavigation}/{givenFileNavigation}/{fileRevision:int?}")]
|
||||
public ActionResult Binary(string givenPageNavigation, string givenFileNavigation, int? fileRevision = null)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationFileRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, fileRevision);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return File(file.Data.ToArray(), file.ContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = 404;
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate the upload page. Shows the attachments.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpGet("Revisions/{givenPageNavigation}/{givenFileNavigation}")]
|
||||
public ActionResult Revisions(string givenPageNavigation, string givenFileNavigation)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
var model = new PageFileRevisionsViewModel()
|
||||
{
|
||||
PageNavigation = pageNavigation.Canonical,
|
||||
FileNavigation = fileNavigation.Canonical,
|
||||
Revisions = PageFileRepository.GetPageFileAttachmentRevisionsByPageAndFileNavigationPaged
|
||||
(pageNavigation.Canonical, fileNavigation.Canonical, GetQueryValue("page", 1))
|
||||
};
|
||||
|
||||
model.PaginationPageCount = (model.Revisions.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Populate the upload page. Shows the attachments.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpGet("PageAttachments/{givenPageNavigation}")]
|
||||
public ActionResult PageAttachments(string givenPageNavigation)
|
||||
{
|
||||
SessionState.RequireCreatePermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation);
|
||||
if (page != null)
|
||||
{
|
||||
var pageFiles = PageFileRepository.GetPageFilesInfoByPageId(page.Id);
|
||||
|
||||
return View(new FileAttachmentViewModel
|
||||
{
|
||||
PageNavigation = page.Navigation,
|
||||
PageRevision = page.Revision,
|
||||
Files = pageFiles
|
||||
});
|
||||
}
|
||||
|
||||
return View(new FileAttachmentViewModel
|
||||
{
|
||||
Files = new()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file by drag drop.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
[HttpPost("UploadDragDrop/{givenPageNavigation}")]
|
||||
public IActionResult UploadDragDrop(string givenPageNavigation, List<IFormFile> postedFiles)
|
||||
{
|
||||
SessionState.RequireCreatePermission();
|
||||
|
||||
try
|
||||
{
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
|
||||
var page = PageRepository.GetPageInfoByNavigation(pageNavigation.Canonical).EnsureNotNull();
|
||||
|
||||
foreach (IFormFile file in postedFiles)
|
||||
{
|
||||
if (file != null)
|
||||
{
|
||||
var fileSize = file.Length;
|
||||
if (fileSize > 0)
|
||||
{
|
||||
if (fileSize > GlobalConfiguration.MaxAttachmentFileSize)
|
||||
{
|
||||
return Json(new { message = $"Could not attach file: [{file.FileName}], too large." });
|
||||
}
|
||||
|
||||
var fileName = HttpUtility.UrlDecode(file.FileName);
|
||||
|
||||
PageFileRepository.UpsertPageFile(new PageFileAttachment()
|
||||
{
|
||||
Data = Utility.ConvertHttpFileToBytes(file),
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
PageId = page.Id,
|
||||
Name = fileName,
|
||||
FileNavigation = Navigation.Clean(fileName),
|
||||
Size = fileSize,
|
||||
ContentType = Utility.GetMimeType(fileName)
|
||||
}, (SessionState.Profile?.UserId).EnsureNotNullOrEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
return Json(new { success = true, message = $"{postedFiles.Count:n0} file{(postedFiles.Count == 0 || postedFiles.Count > 1 ? "s" : string.Empty)}." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExceptionRepository.InsertException(ex, "Failed to upload file.");
|
||||
return StatusCode(500, new { success = false, message = $"An error occurred: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file by manually selecting it for upload.
|
||||
/// </summary>
|
||||
/// <param name="postData"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("ManualUpload/{givenPageNavigation}")]
|
||||
public IActionResult ManualUpload(string givenPageNavigation, IFormFile fileData)
|
||||
{
|
||||
SessionState.RequireCreatePermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
|
||||
var page = PageRepository.GetPageInfoByNavigation(pageNavigation.Canonical).EnsureNotNull();
|
||||
|
||||
if (fileData != null)
|
||||
{
|
||||
var fileSize = fileData.Length;
|
||||
if (fileSize > 0)
|
||||
{
|
||||
if (fileSize > GlobalConfiguration.MaxAttachmentFileSize)
|
||||
{
|
||||
return Content("Could not save the attached file, too large");
|
||||
}
|
||||
|
||||
var fileName = HttpUtility.UrlDecode(fileData.FileName);
|
||||
|
||||
PageFileRepository.UpsertPageFile(new PageFileAttachment()
|
||||
{
|
||||
Data = Utility.ConvertHttpFileToBytes(fileData),
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
PageId = page.Id,
|
||||
Name = fileName,
|
||||
FileNavigation = Navigation.Clean(fileName),
|
||||
Size = fileSize,
|
||||
ContentType = Utility.GetMimeType(fileName)
|
||||
}, (SessionState.Profile?.UserId).EnsureNotNullOrEmpty());
|
||||
|
||||
return Content("Success");
|
||||
}
|
||||
}
|
||||
|
||||
return Content("Failure");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows a user to delete a page attachment from a page.
|
||||
/// </summary>
|
||||
/// <param name="navigation"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("Detach/{givenPageNavigation}/{givenFileNavigation}/{pageRevision}")]
|
||||
public ActionResult Detach(string givenPageNavigation, string givenFileNavigation, int pageRevision)
|
||||
{
|
||||
SessionState.RequireDeletePermission();
|
||||
|
||||
PageFileRepository.DetachPageRevisionAttachment(
|
||||
new NamespaceNavigation(givenPageNavigation).Canonical,
|
||||
new NamespaceNavigation(givenFileNavigation).Canonical, pageRevision);
|
||||
|
||||
return Content("Success");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file from the database, converts it to a PNG with optional scaling and returns it to the client.
|
||||
/// </summary>
|
||||
/// <param name="navigation"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Emoji/{givenPageNavigation}")]
|
||||
public ActionResult Emoji(string givenPageNavigation)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = Navigation.Clean(givenPageNavigation);
|
||||
|
||||
if (string.IsNullOrEmpty(pageNavigation) == false)
|
||||
{
|
||||
string scale = GetQueryValue("Scale", "100");
|
||||
|
||||
string shortcut = $"%%{pageNavigation.ToLower()}%%";
|
||||
var emoji = GlobalConfiguration.Emojis.Where(o => o.Shortcut == shortcut).FirstOrDefault();
|
||||
if (emoji != null)
|
||||
{
|
||||
//Do we have this scale cached already?
|
||||
var scaledImageCacheKey = WikiCacheKey.Build(WikiCache.Category.Emoji, [shortcut, scale]);
|
||||
if (WikiCache.TryGet<ImageCacheItem>(scaledImageCacheKey, out var cachedEmoji))
|
||||
{
|
||||
return File(cachedEmoji.Bytes, cachedEmoji.ContentType);
|
||||
}
|
||||
|
||||
var imageCacheKey = WikiCacheKey.Build(WikiCache.Category.Emoji, [shortcut]);
|
||||
emoji.ImageData = WikiCache.Get<byte[]>(imageCacheKey);
|
||||
if (emoji.ImageData == null)
|
||||
{
|
||||
//We don't get the bytes by default, that would be a lot of RAM for all the thousands of images.
|
||||
emoji.ImageData = EmojiRepository.GetEmojiByName(emoji.Name)?.ImageData;
|
||||
|
||||
if (emoji.ImageData == null)
|
||||
{
|
||||
return NotFound($"Emoji {pageNavigation} was not found");
|
||||
}
|
||||
|
||||
WikiCache.Put(imageCacheKey, emoji.ImageData);
|
||||
}
|
||||
|
||||
if (emoji.ImageData != null)
|
||||
{
|
||||
var decompressedImageBytes = Utility.Decompress(emoji.ImageData);
|
||||
|
||||
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(decompressedImageBytes));
|
||||
|
||||
int customScalePercent = int.Parse(scale);
|
||||
if (customScalePercent > 500)
|
||||
{
|
||||
customScalePercent = 500;
|
||||
}
|
||||
|
||||
var (Width, Height) = Utility.ScaleToMaxOf(img.Width, img.Height, GlobalConfiguration.DefaultEmojiHeight);
|
||||
|
||||
//Adjust to any specified scaling.
|
||||
Height = (int)(Height * (customScalePercent / 100.0));
|
||||
Width = (int)(Width * (customScalePercent / 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)
|
||||
{
|
||||
Height += 16 - Height;
|
||||
Width += 16 - Height;
|
||||
}
|
||||
if (Width < 16)
|
||||
{
|
||||
Height += 16 - Width;
|
||||
Width += 16 - Width;
|
||||
}
|
||||
|
||||
if (emoji.MimeType?.ToLower() == "image/gif")
|
||||
{
|
||||
var resized = ResizeGifImage(decompressedImageBytes, Width, Height);
|
||||
var itemCache = new ImageCacheItem(resized, "image/gif");
|
||||
WikiCache.Put(scaledImageCacheKey, itemCache);
|
||||
return File(itemCache.Bytes, itemCache.ContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = Images.ResizeImage(img, Width, Height);
|
||||
using var ms = new MemoryStream();
|
||||
image.SaveAsPng(ms);
|
||||
|
||||
var itemCache = new ImageCacheItem(ms.ToArray(), "image/png");
|
||||
WikiCache.Put(scaledImageCacheKey, itemCache);
|
||||
|
||||
return File(itemCache.Bytes, itemCache.ContentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NotFound($"Emoji {pageNavigation} was not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
839
TightWiki/Controllers/PageController.cs
Normal file
839
TightWiki/Controllers/PageController.cs
Normal file
@@ -0,0 +1,839 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using SixLabors.ImageSharp;
|
||||
using System.Text;
|
||||
using TightWiki.Caching;
|
||||
using TightWiki.Engine;
|
||||
using TightWiki.Engine.Implementation.Utility;
|
||||
using TightWiki.Engine.Library.Interfaces;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models;
|
||||
using TightWiki.Models.DataModels;
|
||||
using TightWiki.Models.ViewModels.Page;
|
||||
using TightWiki.Repository;
|
||||
using static TightWiki.Library.Constants;
|
||||
using static TightWiki.Library.Images;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Route("")]
|
||||
public class PageController(ITightEngine tightEngine, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: WikiControllerBase(signInManager, userManager)
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[Route("/robots.txt")]
|
||||
public ContentResult RobotsTxt()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("User-agent: *")
|
||||
.AppendLine("Allow: /");
|
||||
|
||||
return Content(sb.ToString(), "text/plain", Encoding.UTF8);
|
||||
}
|
||||
|
||||
#region Display.
|
||||
|
||||
/// <summary>
|
||||
/// Default controller for root requests. e.g. http://127.0.0.1/
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
public IActionResult Display()
|
||||
=> Display("home", null);
|
||||
|
||||
|
||||
[HttpGet("{givenCanonical}/{pageRevision:int?}")]
|
||||
public IActionResult Display(string givenCanonical, int? pageRevision)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var model = new PageDisplayViewModel();
|
||||
var navigation = new NamespaceNavigation(givenCanonical);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(navigation.Canonical, pageRevision);
|
||||
if (page != null)
|
||||
{
|
||||
var instructions = PageRepository.GetPageProcessingInstructionsByPageId(page.Id);
|
||||
model.Revision = page.Revision;
|
||||
model.MostCurrentRevision = page.MostCurrentRevision;
|
||||
model.Name = page.Name;
|
||||
model.Namespace = page.Namespace;
|
||||
model.Navigation = page.Navigation;
|
||||
model.HideFooterComments = instructions.Contains(WikiInstruction.HideFooterComments);
|
||||
model.HideFooterLastModified = instructions.Contains(WikiInstruction.HideFooterLastModified);
|
||||
model.ModifiedByUserName = page.ModifiedByUserName;
|
||||
model.ModifiedDate = SessionState.LocalizeDateTime(page.ModifiedDate);
|
||||
|
||||
SessionState.SetPageId(page.Id, pageRevision);
|
||||
|
||||
if (GlobalConfiguration.PageCacheSeconds > 0)
|
||||
{
|
||||
string queryKey = string.Empty;
|
||||
foreach (var query in Request.Query)
|
||||
{
|
||||
queryKey += $"{query.Key}:{query.Value}";
|
||||
}
|
||||
|
||||
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [page.Navigation, page.Revision, queryKey]);
|
||||
if (WikiCache.TryGet<PageCache>(cacheKey, out var cached))
|
||||
{
|
||||
model.Body = cached.Body;
|
||||
SessionState.PageTitle = cached.PageTitle;
|
||||
WikiCache.Put(cacheKey, cached); //Update the cache expiration.
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = tightEngine.Transform(SessionState, page, pageRevision);
|
||||
SessionState.PageTitle = state.PageTitle;
|
||||
|
||||
model.Body = state.HtmlResult;
|
||||
if (state.ProcessingInstructions.Contains(WikiInstruction.NoCache) == false)
|
||||
{
|
||||
var toBeCached = new PageCache(state.HtmlResult)
|
||||
{
|
||||
PageTitle = state.PageTitle
|
||||
};
|
||||
|
||||
WikiCache.Put(cacheKey, toBeCached); //This is cleared with the call to Cache.ClearCategory($"Page:{page.Navigation}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var state = tightEngine.Transform(SessionState, page, pageRevision);
|
||||
|
||||
model.Body = state.HtmlResult;
|
||||
}
|
||||
|
||||
if (GlobalConfiguration.EnablePageComments && GlobalConfiguration.ShowCommentsOnPageFooter && model.HideFooterComments == false)
|
||||
{
|
||||
var comments = PageRepository.GetPageCommentsPaged(navigation.Canonical, 1);
|
||||
|
||||
foreach (var comment in comments)
|
||||
{
|
||||
model.Comments.Add(new PageComment
|
||||
{
|
||||
PaginationPageCount = comment.PaginationPageCount,
|
||||
UserNavigation = comment.UserNavigation,
|
||||
Id = comment.Id,
|
||||
UserName = comment.UserName,
|
||||
UserId = comment.UserId,
|
||||
Body = WikifierLite.Process(comment.Body),
|
||||
CreatedDate = SessionState.LocalizeDateTime(comment.CreatedDate)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (pageRevision != null)
|
||||
{
|
||||
var notExistPageName = ConfigurationRepository.Get<string>("Customization", "Revision Does Not Exists Page");
|
||||
string notExistPageNavigation = NamespaceNavigation.CleanAndValidate(notExistPageName);
|
||||
var notExistsPage = PageRepository.GetPageRevisionByNavigation(notExistPageNavigation).EnsureNotNull();
|
||||
|
||||
SessionState.SetPageId(null, pageRevision);
|
||||
|
||||
var state = tightEngine.Transform(SessionState, notExistsPage);
|
||||
|
||||
SessionState.Page.Name = notExistsPage.Name;
|
||||
model.Body = state.HtmlResult;
|
||||
|
||||
model.HideFooterComments = true;
|
||||
|
||||
if (SessionState.IsAuthenticated && SessionState.CanCreate)
|
||||
{
|
||||
SessionState.ShouldCreatePage = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var notExistPageName = ConfigurationRepository.Get<string>("Customization", "Page Not Exists Page");
|
||||
string notExistPageNavigation = NamespaceNavigation.CleanAndValidate(notExistPageName);
|
||||
var notExistsPage = PageRepository.GetPageRevisionByNavigation(notExistPageNavigation).EnsureNotNull();
|
||||
|
||||
SessionState.SetPageId(null, null);
|
||||
|
||||
var state = tightEngine.Transform(SessionState, notExistsPage);
|
||||
|
||||
SessionState.Page.Name = notExistsPage.Name;
|
||||
model.Body = state.HtmlResult;
|
||||
|
||||
model.HideFooterComments = true;
|
||||
|
||||
if (SessionState.IsAuthenticated && SessionState.CanCreate)
|
||||
{
|
||||
SessionState.ShouldCreatePage = true;
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Search.
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Page/Search")]
|
||||
public ActionResult Search()
|
||||
{
|
||||
string searchString = GetQueryValue("SearchString") ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(searchString) == false)
|
||||
{
|
||||
var model = new PageSearchViewModel()
|
||||
{
|
||||
Pages = PageRepository.PageSearchPaged(Utility.SplitToTokens(searchString), GetQueryValue("page", 1)),
|
||||
SearchString = searchString
|
||||
};
|
||||
|
||||
model.PaginationPageCount = (model.Pages.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
return View(new PageSearchViewModel()
|
||||
{
|
||||
Pages = new(),
|
||||
SearchString = searchString
|
||||
});
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("Page/Search")]
|
||||
public ActionResult Search(PageSearchViewModel model)
|
||||
{
|
||||
string searchString = GetQueryValue("SearchString") ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(searchString) == false)
|
||||
{
|
||||
model = new PageSearchViewModel()
|
||||
{
|
||||
Pages = PageRepository.PageSearchPaged(Utility.SplitToTokens(searchString), GetQueryValue("page", 1)),
|
||||
SearchString = searchString
|
||||
};
|
||||
|
||||
model.PaginationPageCount = (model.Pages.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
return View(new PageSearchViewModel()
|
||||
{
|
||||
Pages = new(),
|
||||
SearchString = searchString
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Comments.
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("{givenCanonical}/Comments")]
|
||||
public ActionResult Comments(string givenCanonical)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var pageInfo = PageRepository.GetPageInfoByNavigation(pageNavigation);
|
||||
if (pageInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var deleteAction = GetQueryValue("Delete");
|
||||
if (string.IsNullOrEmpty(deleteAction) == false && SessionState.IsAuthenticated)
|
||||
{
|
||||
if (SessionState.CanModerate)
|
||||
{
|
||||
//Moderators and administrators can delete comments that they do not own.
|
||||
PageRepository.DeletePageCommentById(pageInfo.Id, int.Parse(deleteAction));
|
||||
}
|
||||
else
|
||||
{
|
||||
PageRepository.DeletePageCommentByUserAndId(pageInfo.Id, SessionState.Profile.EnsureNotNull().UserId, int.Parse(deleteAction));
|
||||
}
|
||||
}
|
||||
|
||||
var model = new PageCommentsViewModel();
|
||||
|
||||
var comments = PageRepository.GetPageCommentsPaged(pageNavigation, GetQueryValue("page", 1));
|
||||
foreach (var comment in comments)
|
||||
{
|
||||
model.Comments.Add(new PageComment
|
||||
{
|
||||
PaginationPageCount = comment.PaginationPageCount,
|
||||
UserNavigation = comment.UserNavigation,
|
||||
Id = comment.Id,
|
||||
UserName = comment.UserName,
|
||||
UserId = comment.UserId,
|
||||
Body = WikifierLite.Process(comment.Body),
|
||||
CreatedDate = SessionState.LocalizeDateTime(comment.CreatedDate)
|
||||
});
|
||||
}
|
||||
|
||||
model.PaginationPageCount = (model.Comments.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
SessionState.SetPageId(pageInfo.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert new page comment.
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="givenCanonical"></param>
|
||||
/// <param name="page"></param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpPost("{givenCanonical}/Comments")]
|
||||
public ActionResult Comments(PageCommentsViewModel model, string givenCanonical)
|
||||
{
|
||||
SessionState.RequireEditPermission();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
string? errorMessage = null;
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var pageInfo = PageRepository.GetPageInfoByNavigation(pageNavigation);
|
||||
if (pageInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
PageRepository.InsertPageComment(pageInfo.Id, SessionState.Profile.EnsureNotNull().UserId, model.Comment);
|
||||
|
||||
model = new PageCommentsViewModel()
|
||||
{
|
||||
ErrorMessage = errorMessage.DefaultWhenNull(string.Empty)
|
||||
};
|
||||
|
||||
var comments = PageRepository.GetPageCommentsPaged(pageNavigation, GetQueryValue("page", 1));
|
||||
foreach (var comment in comments)
|
||||
{
|
||||
model.Comments.Add(new PageComment
|
||||
{
|
||||
PaginationPageCount = comment.PaginationPageCount,
|
||||
UserNavigation = comment.UserNavigation,
|
||||
Id = comment.Id,
|
||||
UserName = comment.UserName,
|
||||
UserId = comment.UserId,
|
||||
Body = WikifierLite.Process(comment.Body),
|
||||
CreatedDate = SessionState.LocalizeDateTime(comment.CreatedDate)
|
||||
});
|
||||
}
|
||||
|
||||
model.PaginationPageCount = (model.Comments.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
SessionState.SetPageId(pageInfo.Id);
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Refresh.
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{givenCanonical}/Refresh")]
|
||||
public ActionResult Refresh(string givenCanonical)
|
||||
{
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation, null, false);
|
||||
|
||||
if (page != null)
|
||||
{
|
||||
Engine.Implementation.Helpers.RefreshPageMetadata(tightEngine, page, SessionState);
|
||||
}
|
||||
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/{pageNavigation}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Revisions.
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{givenCanonical}/Revisions")]
|
||||
public ActionResult Revisions(string givenCanonical)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var pageNumber = GetQueryValue("page", 1);
|
||||
var orderBy = GetQueryValue("OrderBy");
|
||||
var orderByDirection = GetQueryValue("OrderByDirection");
|
||||
|
||||
var model = new RevisionsViewModel()
|
||||
{
|
||||
Revisions = PageRepository.GetPageRevisionsInfoByNavigationPaged(pageNavigation, pageNumber, orderBy, orderByDirection)
|
||||
};
|
||||
|
||||
model.PaginationPageCount = (model.Revisions.FirstOrDefault()?.PaginationPageCount ?? 0);
|
||||
|
||||
model.Revisions.ForEach(o =>
|
||||
{
|
||||
o.CreatedDate = SessionState.LocalizeDateTime(o.CreatedDate);
|
||||
o.ModifiedDate = SessionState.LocalizeDateTime(o.ModifiedDate);
|
||||
});
|
||||
|
||||
foreach (var p in model.Revisions)
|
||||
{
|
||||
var thisRev = PageRepository.GetPageRevisionByNavigation(p.Navigation, p.Revision);
|
||||
var prevRev = PageRepository.GetPageRevisionByNavigation(p.Navigation, p.Revision - 1);
|
||||
p.ChangeSummary = Differentiator.GetComparisonSummary(thisRev?.Body ?? "", prevRev?.Body ?? "");
|
||||
}
|
||||
|
||||
if (model.Revisions != null && model.Revisions.Count > 0)
|
||||
{
|
||||
SessionState.SetPageId(model.Revisions.First().PageId);
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete.
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("{givenCanonical}/Delete")]
|
||||
public ActionResult Delete(string givenCanonical, PageDeleteViewModel model)
|
||||
{
|
||||
SessionState.RequireDeletePermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation);
|
||||
var instructions = PageRepository.GetPageProcessingInstructionsByPageId(page.EnsureNotNull().Id);
|
||||
if (instructions.Contains(WikiInstruction.Protect))
|
||||
{
|
||||
return NotifyOfError("The page is protected and cannot be deleted. A moderator or an administrator must remove the protection before deletion.");
|
||||
}
|
||||
|
||||
bool confirmAction = bool.Parse(GetFormValue("IsActionConfirmed").EnsureNotNull());
|
||||
if (confirmAction == true && page != null)
|
||||
{
|
||||
PageRepository.MovePageToDeletedById(page.Id, (SessionState.Profile?.UserId).EnsureNotNullOrEmpty());
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [page.Navigation]));
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [page.Id]));
|
||||
return NotifyOfSuccess("The page has been deleted.", $"/Home");
|
||||
}
|
||||
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/{pageNavigation}");
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{givenCanonical}/Delete")]
|
||||
public ActionResult Delete(string givenCanonical)
|
||||
{
|
||||
SessionState.RequireDeletePermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation).EnsureNotNull();
|
||||
|
||||
var model = new PageDeleteViewModel()
|
||||
{
|
||||
CountOfAttachments = PageRepository.GetCountOfPageAttachmentsById(page.Id),
|
||||
PageName = page.Name,
|
||||
MostCurrentRevision = page.Revision,
|
||||
PageRevision = page.Revision
|
||||
};
|
||||
|
||||
SessionState.SetPageId(page.Id);
|
||||
|
||||
var instructions = PageRepository.GetPageProcessingInstructionsByPageId(page.Id);
|
||||
if (instructions.Contains(WikiInstruction.Protect))
|
||||
{
|
||||
return NotifyOfError("The page is protected and cannot be deleted. A moderator or an administrator must remove the protection before deletion.");
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Revert.
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("{givenCanonical}/Revert/{pageRevision:int}")]
|
||||
public ActionResult Revert(string givenCanonical, int pageRevision, PageRevertViewModel model)
|
||||
{
|
||||
SessionState.RequireModeratePermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
bool confirmAction = bool.Parse(GetFormValue("IsActionConfirmed").EnsureNotNullOrEmpty());
|
||||
if (confirmAction == true)
|
||||
{
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation, pageRevision).EnsureNotNull();
|
||||
Engine.Implementation.Helpers.UpsertPage(tightEngine, page, SessionState);
|
||||
return NotifyOfSuccess("The page has been reverted.", $"/{pageNavigation}");
|
||||
}
|
||||
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/{pageNavigation}");
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{givenCanonical}/Revert/{pageRevision:int}")]
|
||||
public ActionResult Revert(string givenCanonical, int pageRevision)
|
||||
{
|
||||
SessionState.RequireModeratePermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var mostCurrentPage = PageRepository.GetPageRevisionByNavigation(pageNavigation).EnsureNotNull();
|
||||
mostCurrentPage.CreatedDate = SessionState.LocalizeDateTime(mostCurrentPage.CreatedDate);
|
||||
mostCurrentPage.ModifiedDate = SessionState.LocalizeDateTime(mostCurrentPage.ModifiedDate);
|
||||
|
||||
var revisionPage = PageRepository.GetPageRevisionByNavigation(pageNavigation, pageRevision).EnsureNotNull();
|
||||
revisionPage.CreatedDate = SessionState.LocalizeDateTime(revisionPage.CreatedDate);
|
||||
revisionPage.ModifiedDate = SessionState.LocalizeDateTime(revisionPage.ModifiedDate);
|
||||
|
||||
var model = new PageRevertViewModel()
|
||||
{
|
||||
PageName = revisionPage.Name,
|
||||
HighestRevision = mostCurrentPage.Revision,
|
||||
HigherRevisionCount = revisionPage.HigherRevisionCount,
|
||||
};
|
||||
|
||||
if (revisionPage != null)
|
||||
{
|
||||
SessionState.SetPageId(revisionPage.Id, pageRevision);
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edit.
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("{givenCanonical}/Edit")]
|
||||
[HttpGet("Page/Create")]
|
||||
public ActionResult Edit(string givenCanonical)
|
||||
{
|
||||
SessionState.RequireEditPermission();
|
||||
|
||||
var pageNavigation = NamespaceNavigation.CleanAndValidate(givenCanonical);
|
||||
|
||||
var page = PageRepository.GetPageRevisionByNavigation(pageNavigation);
|
||||
if (page != null)
|
||||
{
|
||||
var instructions = PageRepository.GetPageProcessingInstructionsByPageId(page.EnsureNotNull().Id);
|
||||
if (SessionState.CanModerate == false && instructions.Contains(WikiInstruction.Protect))
|
||||
{
|
||||
return NotifyOfError("The page is protected and cannot be modified except by a moderator or an administrator unless the protection is removed.");
|
||||
}
|
||||
|
||||
SessionState.SetPageId(page.Id);
|
||||
|
||||
return View(new PageEditViewModel()
|
||||
{
|
||||
Id = page.Id,
|
||||
Body = page.Body,
|
||||
Name = page.Name,
|
||||
Navigation = NamespaceNavigation.CleanAndValidate(page.Navigation),
|
||||
Description = page.Description
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
var pageName = GetQueryValue("Name").DefaultWhenNullOrEmpty(pageNavigation);
|
||||
|
||||
string templateName = ConfigurationRepository.Get<string>("Customization", "New Page Template").EnsureNotNull();
|
||||
string templateNavigation = NamespaceNavigation.CleanAndValidate(templateName);
|
||||
var templatePage = PageRepository.GetPageRevisionByNavigation(templateNavigation);
|
||||
|
||||
templatePage ??= new Page();
|
||||
|
||||
return View(new PageEditViewModel()
|
||||
{
|
||||
Body = templatePage.Body,
|
||||
Name = pageName?.Replace('_', ' ') ?? string.Empty,
|
||||
Navigation = NamespaceNavigation.CleanAndValidate(pageNavigation)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("{givenCanonical}/Edit")]
|
||||
[HttpPost("Page/Create")]
|
||||
public ActionResult Edit(PageEditViewModel model)
|
||||
{
|
||||
SessionState.RequireEditPermission();
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (model.Id == 0) //Saving a new page.
|
||||
{
|
||||
var page = new Page()
|
||||
{
|
||||
CreatedDate = DateTime.UtcNow,
|
||||
CreatedByUserId = SessionState.Profile.EnsureNotNull().UserId,
|
||||
ModifiedDate = DateTime.UtcNow,
|
||||
ModifiedByUserId = SessionState.Profile.UserId,
|
||||
Body = model.Body ?? "",
|
||||
Name = model.Name,
|
||||
Navigation = NamespaceNavigation.CleanAndValidate(model.Name),
|
||||
Description = model.Description ?? ""
|
||||
};
|
||||
|
||||
if (PageRepository.GetPageInfoByNavigation(page.Navigation) != null)
|
||||
{
|
||||
ModelState.AddModelError("Name", "The page name you entered already exists.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
page.Id = Engine.Implementation.Helpers.UpsertPage(tightEngine, page, SessionState);
|
||||
|
||||
SessionState.SetPageId(page.Id);
|
||||
|
||||
return NotifyOfSuccess("The page has been created.", $"/{page.Navigation}/Edit");
|
||||
}
|
||||
else
|
||||
{
|
||||
var page = PageRepository.GetPageRevisionById(model.Id).EnsureNotNull();
|
||||
var instructions = PageRepository.GetPageProcessingInstructionsByPageId(page.Id);
|
||||
if (SessionState.CanModerate == false && instructions.Contains(WikiInstruction.Protect))
|
||||
{
|
||||
return NotifyOfError("The page is protected and cannot be modified except by a moderator or an administrator unless the protection is removed.");
|
||||
}
|
||||
|
||||
string originalNavigation = string.Empty;
|
||||
|
||||
model.Navigation = NamespaceNavigation.CleanAndValidate(model.Name);
|
||||
|
||||
if (!page.Navigation.Equals(model.Navigation, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
if (PageRepository.GetPageInfoByNavigation(model.Navigation) != null)
|
||||
{
|
||||
ModelState.AddModelError("Name", "The page name you entered already exists.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
originalNavigation = page.Navigation; //So we can clear cache and this also indicates that we need to redirect to the new name.
|
||||
}
|
||||
|
||||
page.ModifiedDate = DateTime.UtcNow;
|
||||
page.ModifiedByUserId = SessionState.Profile.EnsureNotNull().UserId;
|
||||
page.Body = model.Body ?? "";
|
||||
page.Name = model.Name;
|
||||
page.Navigation = NamespaceNavigation.CleanAndValidate(model.Name);
|
||||
page.Description = model.Description ?? "";
|
||||
|
||||
Engine.Implementation.Helpers.UpsertPage(tightEngine, page, SessionState);
|
||||
|
||||
SessionState.SetPageId(page.Id);
|
||||
|
||||
model.SuccessMessage = "The page was saved.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(originalNavigation) == false)
|
||||
{
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [originalNavigation]));
|
||||
WikiCache.ClearCategory(WikiCacheKey.Build(WikiCache.Category.Page, [page.Id]));
|
||||
return Redirect($"{GlobalConfiguration.BasePath}/{page.Navigation}/Edit");
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region File.
|
||||
|
||||
/// <summary>
|
||||
/// Gets an image attached to a page.
|
||||
/// </summary>
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="pageRevision">The revision of the the PAGE that the file is attached to (NOT THE FILE REVISION)</param>
|
||||
/// <returns></returns>
|
||||
[HttpGet("Page/Image/{givenPageNavigation}/{givenFileNavigation}/{pageRevision:int?}")]
|
||||
public ActionResult Image(string givenPageNavigation, string givenFileNavigation, int? pageRevision = null)
|
||||
{
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
string givenScale = GetQueryValue("Scale", "100");
|
||||
|
||||
var cacheKey = WikiCacheKeyFunction.Build(WikiCache.Category.Page, [givenPageNavigation, givenFileNavigation, pageRevision, givenScale]);
|
||||
if (WikiCache.TryGet<ImageCacheItem>(cacheKey, out var cached))
|
||||
{
|
||||
return File(cached.Bytes, cached.ContentType);
|
||||
}
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationPageRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, pageRevision);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
if (file.ContentType == "image/x-icon")
|
||||
{
|
||||
//We do not handle the resizing of icon file. Maybe later....
|
||||
return File(file.Data, file.ContentType);
|
||||
}
|
||||
|
||||
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(file.Data));
|
||||
|
||||
int parsedScale = int.Parse(givenScale);
|
||||
if (parsedScale > 500)
|
||||
{
|
||||
parsedScale = 500;
|
||||
}
|
||||
if (parsedScale != 100)
|
||||
{
|
||||
int width = (int)(img.Width * (parsedScale / 100.0));
|
||||
int 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)
|
||||
{
|
||||
height += 16 - height;
|
||||
width += 16 - height;
|
||||
}
|
||||
if (width < 16)
|
||||
{
|
||||
height += 16 - width;
|
||||
width += 16 - width;
|
||||
}
|
||||
|
||||
if (file.ContentType.Equals("image/gif", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var resized = ResizeGifImage(file.Data, width, height);
|
||||
return File(resized, "image/gif");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var image = ResizeImage(img, width, height);
|
||||
using var ms = new MemoryStream();
|
||||
file.ContentType = BestEffortConvertImage(image, ms, file.ContentType);
|
||||
var cacheItem = new ImageCacheItem(ms.ToArray(), file.ContentType);
|
||||
WikiCache.Put(cacheKey, cacheItem);
|
||||
return File(cacheItem.Bytes, cacheItem.ContentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return File(file.Data, file.ContentType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an image from the database, converts it to a PNG with optional scaling and returns it to the client.
|
||||
/// </summary>
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="pageRevision">The revision of the the PAGE that the file is attached to (NOT THE FILE REVISION)</param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Page/Png/{givenPageNavigation}/{givenFileNavigation}/{pageRevision:int?}")]
|
||||
public ActionResult Png(string givenPageNavigation, string givenFileNavigation, int? pageRevision = null)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
string givenScale = GetQueryValue("Scale", "100");
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationPageRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, pageRevision);
|
||||
if (file != null)
|
||||
{
|
||||
var img = SixLabors.ImageSharp.Image.Load(new MemoryStream(Utility.Decompress(file.Data)));
|
||||
|
||||
int parsedScale = int.Parse(givenScale);
|
||||
if (parsedScale > 500)
|
||||
{
|
||||
parsedScale = 500;
|
||||
}
|
||||
|
||||
if (parsedScale != 100)
|
||||
{
|
||||
int width = (int)(img.Width * (parsedScale / 100.0));
|
||||
int 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)
|
||||
{
|
||||
height += 16 - height;
|
||||
width += 16 - height;
|
||||
}
|
||||
if (width < 16)
|
||||
{
|
||||
height += 16 - width;
|
||||
width += 16 - width;
|
||||
}
|
||||
|
||||
using var image = Images.ResizeImage(img, width, height);
|
||||
using var ms = new MemoryStream();
|
||||
image.SaveAsPng(ms);
|
||||
return File(ms.ToArray(), "image/png");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
img.SaveAsPng(ms);
|
||||
return File(ms.ToArray(), "image/png");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a file from the database and returns it to the client.
|
||||
/// <param name="givenPageNavigation">The navigation link of the page.</param>
|
||||
/// <param name="givenFileNavigation">The navigation link of the file.</param>
|
||||
/// <param name="pageRevision">The revision of the the PAGE that the file is attached to (NOT THE FILE REVISION)</param>
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Page/Binary/{givenPageNavigation}/{givenFileNavigation}/{pageRevision:int?}")]
|
||||
public ActionResult Binary(string givenPageNavigation, string givenFileNavigation, int? pageRevision = null)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var pageNavigation = new NamespaceNavigation(givenPageNavigation);
|
||||
var fileNavigation = new NamespaceNavigation(givenFileNavigation);
|
||||
|
||||
var file = PageFileRepository.GetPageFileAttachmentByPageNavigationPageRevisionAndFileNavigation(pageNavigation.Canonical, fileNavigation.Canonical, pageRevision);
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return File(file.Data.ToArray(), file.ContentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = 404;
|
||||
return NotFound($"[{fileNavigation}] was not found on the page [{pageNavigation}].");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
70
TightWiki/Controllers/TagsController.cs
Normal file
70
TightWiki/Controllers/TagsController.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Text;
|
||||
using TightWiki.Engine.Implementation.Utility;
|
||||
using TightWiki.Library;
|
||||
using TightWiki.Models.ViewModels.Page;
|
||||
using TightWiki.Repository;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
public class TagsController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: WikiControllerBase(signInManager, userManager)
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public ActionResult Browse(string navigation)
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
SessionState.Page.Name = "Tags";
|
||||
|
||||
navigation = NamespaceNavigation.CleanAndValidate(navigation);
|
||||
|
||||
string glossaryName = "glossary_" + (new Random()).Next(0, 1000000).ToString();
|
||||
var pages = PageRepository.GetPageInfoByTag(navigation).OrderBy(o => o.Name).ToList();
|
||||
var glossaryHtml = new StringBuilder();
|
||||
var alphabet = pages.Select(p => p.Name.Substring(0, 1).ToUpper()).Distinct();
|
||||
|
||||
if (pages.Count > 0)
|
||||
{
|
||||
glossaryHtml.Append("<center>");
|
||||
foreach (var alpha in alphabet)
|
||||
{
|
||||
glossaryHtml.Append("<a href=\"#" + glossaryName + "_" + alpha + "\">" + alpha + "</a> ");
|
||||
}
|
||||
glossaryHtml.Append("</center>");
|
||||
|
||||
glossaryHtml.Append("<ul>");
|
||||
foreach (var alpha in alphabet)
|
||||
{
|
||||
glossaryHtml.Append("<li><a name=\"" + glossaryName + "_" + alpha + "\">" + alpha + "</a></li>");
|
||||
|
||||
glossaryHtml.Append("<ul>");
|
||||
foreach (var page in pages.Where(p => p.Name.StartsWith(alpha, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
glossaryHtml.Append("<li><a href=\"/" + page.Navigation + "\">" + page.Name + "</a>");
|
||||
|
||||
if (page.Description.Length > 0)
|
||||
{
|
||||
glossaryHtml.Append(" - " + page.Description);
|
||||
}
|
||||
glossaryHtml.Append("</li>");
|
||||
}
|
||||
glossaryHtml.Append("</ul>");
|
||||
}
|
||||
|
||||
glossaryHtml.Append("</ul>");
|
||||
}
|
||||
|
||||
var model = new BrowseViewModel
|
||||
{
|
||||
AssociatedPages = glossaryHtml.ToString(),
|
||||
TagCloud = TagCloud.Build(navigation, 100)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
TightWiki/Controllers/UtilityController.cs
Normal file
54
TightWiki/Controllers/UtilityController.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NTDLS.Helpers;
|
||||
using TightWiki.Models.ViewModels.Utility;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
[Authorize]
|
||||
[Route("[controller]")]
|
||||
public class UtilityController(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: WikiControllerBase(signInManager, userManager)
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[HttpGet("Notify")]
|
||||
public ActionResult Notify()
|
||||
{
|
||||
SessionState.RequireViewPermission();
|
||||
|
||||
var model = new NotifyViewModel()
|
||||
{
|
||||
SuccessMessage = GetQueryValue("SuccessMessage", string.Empty),
|
||||
ErrorMessage = GetQueryValue("ErrorMessage", string.Empty),
|
||||
RedirectURL = GetQueryValue("RedirectURL", string.Empty),
|
||||
RedirectTimeout = GetQueryValue("RedirectTimeout", 0)
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("ConfirmAction")]
|
||||
public ActionResult ConfirmAction(ConfirmActionViewModel model)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("ConfirmAction")]
|
||||
public ActionResult ConfirmAction()
|
||||
{
|
||||
var model = new ConfirmActionViewModel
|
||||
{
|
||||
ControllerURL = GetQueryValue("controllerURL").EnsureNotNull(),
|
||||
YesRedirectURL = GetQueryValue("yesRedirectURL").EnsureNotNull(),
|
||||
NoRedirectURL = GetQueryValue("noRedirectURL").EnsureNotNull(),
|
||||
Message = GetQueryValue("message").EnsureNotNull(),
|
||||
Style = GetQueryValue("Style").EnsureNotNull()
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
TightWiki/Controllers/WikiControllerBase.cs
Normal file
71
TightWiki/Controllers/WikiControllerBase.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using NTDLS.Helpers;
|
||||
using TightWiki.Models;
|
||||
|
||||
namespace TightWiki.Controllers
|
||||
{
|
||||
public class WikiControllerBase(SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
|
||||
: Controller
|
||||
{
|
||||
public SessionState SessionState { get; private set; } = new();
|
||||
|
||||
public readonly SignInManager<IdentityUser> SignInManager = signInManager;
|
||||
public readonly UserManager<IdentityUser> UserManager = userManager;
|
||||
|
||||
[NonAction]
|
||||
public override void OnActionExecuting(ActionExecutingContext filterContext)
|
||||
=> ViewData["SessionState"] = SessionState.Hydrate(SignInManager, this);
|
||||
|
||||
[NonAction]
|
||||
public override RedirectResult Redirect(string? url)
|
||||
=> base.Redirect(url.EnsureNotNull());
|
||||
|
||||
[NonAction]
|
||||
protected string? GetQueryValue(string key)
|
||||
=> Request.Query[key];
|
||||
|
||||
[NonAction]
|
||||
protected string GetQueryValue(string key, string defaultValue)
|
||||
=> (string?)Request.Query[key] ?? defaultValue;
|
||||
|
||||
[NonAction]
|
||||
protected int GetQueryValue(string key, int defaultValue)
|
||||
=> int.Parse(GetQueryValue(key, defaultValue.ToString()));
|
||||
|
||||
[NonAction]
|
||||
protected string? GetFormValue(string key)
|
||||
=> Request.Form[key];
|
||||
|
||||
[NonAction]
|
||||
protected string GetFormValue(string key, string defaultValue)
|
||||
=> (string?)Request.Form[key] ?? defaultValue;
|
||||
|
||||
[NonAction]
|
||||
protected int GetFormValue(string key, int defaultValue)
|
||||
=> int.Parse(GetFormValue(key, defaultValue.ToString()));
|
||||
|
||||
/// <summary>
|
||||
/// Displays the successMessage unless the errorMessage is present.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected RedirectResult NotifyOf(string successMessage, string errorMessage, string redirectUrl)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?SuccessMessage={Uri.EscapeDataString(successMessage)}&ErrorMessage={Uri.EscapeDataString(errorMessage)}&RedirectUrl={Uri.EscapeDataString($"{GlobalConfiguration.BasePath}{redirectUrl}")}&RedirectTimeout=5");
|
||||
|
||||
protected RedirectResult NotifyOfSuccess(string message, string redirectUrl)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?SuccessMessage={Uri.EscapeDataString(message)}&RedirectUrl={Uri.EscapeDataString($"{GlobalConfiguration.BasePath}{redirectUrl}")}&RedirectTimeout=5");
|
||||
|
||||
protected RedirectResult NotifyOfError(string message, string redirectUrl)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?ErrorMessage={Uri.EscapeDataString(message)}&RedirectUrl={Uri.EscapeDataString(Uri.EscapeDataString($"{GlobalConfiguration.BasePath}{redirectUrl}"))}");
|
||||
|
||||
protected RedirectResult NotifyOf(string successMessage, string errorMessage)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?SuccessMessage={Uri.EscapeDataString(successMessage)}&ErrorMessage={Uri.EscapeDataString(errorMessage)}&RedirectTimeout=5");
|
||||
|
||||
protected RedirectResult NotifyOfSuccess(string message)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?SuccessMessage={Uri.EscapeDataString(message)}");
|
||||
|
||||
protected RedirectResult NotifyOfError(string message)
|
||||
=> Redirect($"{GlobalConfiguration.BasePath}/Utility/Notify?ErrorMessage={Uri.EscapeDataString(message)}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user