diff --git a/src/Moonglade.Core/DeleteStyleSheetCommand.cs b/src/Moonglade.Core/DeleteStyleSheetCommand.cs new file mode 100644 index 000000000..2f7410ded --- /dev/null +++ b/src/Moonglade.Core/DeleteStyleSheetCommand.cs @@ -0,0 +1,21 @@ +namespace Moonglade.Core; + +public record DeleteStyleSheetCommand(Guid Id) : IRequest; + +public class DeleteStyleSheetCommandHandler : IRequestHandler +{ + private readonly IRepository _repo; + + public DeleteStyleSheetCommandHandler(IRepository repo) => _repo = repo; + + public async Task Handle(DeleteStyleSheetCommand request, CancellationToken cancellationToken) + { + var styleSheet = await _repo.GetAsync(request.Id, cancellationToken); + if (styleSheet is null) + { + throw new InvalidOperationException($"StyleSheetEntity with Id '{request.Id}' not found."); + } + + await _repo.DeleteAsync(styleSheet, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Moonglade.Core/GetStyleSheetQuery.cs b/src/Moonglade.Core/GetStyleSheetQuery.cs new file mode 100644 index 000000000..bdfd0745c --- /dev/null +++ b/src/Moonglade.Core/GetStyleSheetQuery.cs @@ -0,0 +1,16 @@ +namespace Moonglade.Core; + +public record GetStyleSheetQuery(Guid Id) : IRequest; + +public class GetStyleSheetQueryHandler : IRequestHandler +{ + private readonly IRepository _repo; + + public GetStyleSheetQueryHandler(IRepository repo) => _repo = repo; + + public async Task Handle(GetStyleSheetQuery request, CancellationToken cancellationToken) + { + var result = await _repo.GetAsync(request.Id, cancellationToken); + return result; + } +} \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/BlogPage.cs b/src/Moonglade.Core/PageFeature/BlogPage.cs index b33d886e4..4ace8f98a 100644 --- a/src/Moonglade.Core/PageFeature/BlogPage.cs +++ b/src/Moonglade.Core/PageFeature/BlogPage.cs @@ -7,7 +7,7 @@ public class BlogPage public string Slug { get; set; } public string MetaDescription { get; set; } public string RawHtmlContent { get; set; } - public string CssContent { get; set; } + public string CssId { get; set; } public bool HideSidebar { get; set; } public bool IsPublished { get; set; } public DateTime CreateTimeUtc { get; set; } @@ -25,7 +25,7 @@ public BlogPage(PageEntity entity) Id = entity.Id; Title = entity.Title.Trim(); CreateTimeUtc = entity.CreateTimeUtc; - CssContent = entity.CssContent; + CssId = entity.CssId; RawHtmlContent = entity.HtmlContent; HideSidebar = entity.HideSidebar; Slug = entity.Slug.Trim().ToLower(); diff --git a/src/Moonglade.Core/PageFeature/CreatePageCommand.cs b/src/Moonglade.Core/PageFeature/CreatePageCommand.cs index b3354bdde..93bd39084 100644 --- a/src/Moonglade.Core/PageFeature/CreatePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/CreatePageCommand.cs @@ -5,22 +5,36 @@ public record CreatePageCommand(EditPageRequest Payload) : IRequest; public class CreatePageCommandHandler : IRequestHandler { private readonly IRepository _repo; - public CreatePageCommandHandler(IRepository repo) => _repo = repo; + private readonly IMediator _mediator; + + public CreatePageCommandHandler(IRepository repo, IMediator mediator) + { + _repo = repo; + _mediator = mediator; + } public async Task Handle(CreatePageCommand request, CancellationToken ct) { + var slug = request.Payload.Slug.ToLower().Trim(); + + Guid? cssId = null; + if (!string.IsNullOrWhiteSpace(request.Payload.CssContent)) + { + cssId = await _mediator.Send(new SaveStyleSheetCommand(Guid.NewGuid(), slug, request.Payload.CssContent), ct); + } + var uid = Guid.NewGuid(); var page = new PageEntity { Id = uid, Title = request.Payload.Title.Trim(), - Slug = request.Payload.Slug.ToLower().Trim(), + Slug = slug, MetaDescription = request.Payload.MetaDescription, CreateTimeUtc = DateTime.UtcNow, HtmlContent = request.Payload.RawHtmlContent, - CssContent = request.Payload.CssContent, HideSidebar = request.Payload.HideSidebar, - IsPublished = request.Payload.IsPublished + IsPublished = request.Payload.IsPublished, + CssId = cssId.ToString() }; await _repo.AddAsync(page, ct); diff --git a/src/Moonglade.Core/PageFeature/DeletePageCommand.cs b/src/Moonglade.Core/PageFeature/DeletePageCommand.cs index fbba94f6b..8c68a5458 100644 --- a/src/Moonglade.Core/PageFeature/DeletePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/DeletePageCommand.cs @@ -5,8 +5,27 @@ public record DeletePageCommand(Guid Id) : IRequest; public class DeletePageCommandHandler : IRequestHandler { private readonly IRepository _repo; - public DeletePageCommandHandler(IRepository repo) => _repo = repo; + private readonly IMediator _mediator; + + public DeletePageCommandHandler(IRepository repo, IMediator mediator) + { + _repo = repo; + _mediator = mediator; + } + + public async Task Handle(DeletePageCommand request, CancellationToken ct) + { + var page = await _repo.GetAsync(request.Id, ct); + if (page is null) + { + throw new InvalidOperationException($"PageEntity with Id '{request.Id}' not found."); + } + + if (page.CssId != null) + { + await _mediator.Send(new DeleteStyleSheetCommand(new(page.CssId)), ct); + } - public async Task Handle(DeletePageCommand request, CancellationToken ct) => await _repo.DeleteAsync(request.Id, ct); + } } \ No newline at end of file diff --git a/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs b/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs index 4afa8521e..f6fed7e07 100644 --- a/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs +++ b/src/Moonglade.Core/PageFeature/UpdatePageCommand.cs @@ -5,8 +5,13 @@ public record UpdatePageCommand(Guid Id, EditPageRequest Payload) : IRequest { private readonly IRepository _repo; + private readonly IMediator _mediator; - public UpdatePageCommandHandler(IRepository repo) => _repo = repo; + public UpdatePageCommandHandler(IRepository repo, IMediator mediator) + { + _repo = repo; + _mediator = mediator; + } public async Task Handle(UpdatePageCommand request, CancellationToken ct) { @@ -17,14 +22,22 @@ public async Task Handle(UpdatePageCommand request, CancellationToken ct) throw new InvalidOperationException($"PageEntity with Id '{guid}' not found."); } + var slug = request.Payload.Slug.ToLower().Trim(); + + Guid? cssId = null; + if (!string.IsNullOrWhiteSpace(request.Payload.CssContent)) + { + cssId = await _mediator.Send(new SaveStyleSheetCommand(page.Id, slug, request.Payload.CssContent), ct); + } + page.Title = payload.Title.Trim(); - page.Slug = payload.Slug.ToLower().Trim(); + page.Slug = slug; page.MetaDescription = payload.MetaDescription; page.HtmlContent = payload.RawHtmlContent; - page.CssContent = payload.CssContent; page.HideSidebar = payload.HideSidebar; page.UpdateTimeUtc = DateTime.UtcNow; page.IsPublished = payload.IsPublished; + page.CssId = cssId.ToString(); await _repo.UpdateAsync(page, ct); diff --git a/src/Moonglade.Core/SaveStyleSheetCommand.cs b/src/Moonglade.Core/SaveStyleSheetCommand.cs index a4d257e66..344d7c56e 100644 --- a/src/Moonglade.Core/SaveStyleSheetCommand.cs +++ b/src/Moonglade.Core/SaveStyleSheetCommand.cs @@ -1,9 +1,8 @@ using System.Security.Cryptography; -using Moonglade.Data.Spec; namespace Moonglade.Core; -public record SaveStyleSheetCommand(string Slug, string CssContent) : IRequest; +public record SaveStyleSheetCommand(Guid Id, string Slug, string CssContent) : IRequest; public class SaveStyleSheetCommandHandler : IRequestHandler { @@ -17,13 +16,13 @@ public async Task Handle(SaveStyleSheetCommand request, CancellationToken var css = request.CssContent.Trim(); var hash = CalculateHash($"{slug}_{css}"); - var entity = await _repo.GetAsync(new StyleSheetByFriendlyNameSpec(slug), cancellationToken); + var entity = await _repo.GetAsync(request.Id, cancellationToken); if (entity is null) { entity = new() { - Id = Guid.NewGuid(), - FriendlyName = slug, + Id = request.Id, + FriendlyName = $"page_{slug}", CssContent = css, Hash = hash, LastModifiedTimeUtc = DateTime.UtcNow @@ -33,6 +32,7 @@ public async Task Handle(SaveStyleSheetCommand request, CancellationToken } else { + entity.FriendlyName = $"page_{slug}"; entity.CssContent = css; entity.Hash = hash; entity.LastModifiedTimeUtc = DateTime.UtcNow; diff --git a/src/Moonglade.Data.MySql/Configurations/PageConfiguration.cs b/src/Moonglade.Data.MySql/Configurations/PageConfiguration.cs index 0fbcad7d6..7c31fc043 100644 --- a/src/Moonglade.Data.MySql/Configurations/PageConfiguration.cs +++ b/src/Moonglade.Data.MySql/Configurations/PageConfiguration.cs @@ -11,6 +11,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Id).ValueGeneratedNever(); builder.Property(e => e.Title).HasMaxLength(128); builder.Property(e => e.Slug).HasMaxLength(128); + builder.Property(e => e.CssId).HasMaxLength(64); builder.Property(e => e.MetaDescription).HasMaxLength(256); builder.Property(e => e.CreateTimeUtc).HasColumnType("datetime"); builder.Property(e => e.UpdateTimeUtc).HasColumnType("datetime"); diff --git a/src/Moonglade.Data.PostgreSql/Configurations/PageConfiguration.cs b/src/Moonglade.Data.PostgreSql/Configurations/PageConfiguration.cs index bf20f42ae..3024456ff 100644 --- a/src/Moonglade.Data.PostgreSql/Configurations/PageConfiguration.cs +++ b/src/Moonglade.Data.PostgreSql/Configurations/PageConfiguration.cs @@ -11,6 +11,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Id).ValueGeneratedNever(); builder.Property(e => e.Title).HasMaxLength(128); builder.Property(e => e.Slug).HasMaxLength(128); + builder.Property(e => e.CssId).HasMaxLength(64); builder.Property(e => e.MetaDescription).HasMaxLength(256); builder.Property(e => e.CreateTimeUtc).HasColumnType("timestamp"); builder.Property(e => e.UpdateTimeUtc).HasColumnType("timestamp"); diff --git a/src/Moonglade.Data.SqlServer/Configurations/PageConfiguration.cs b/src/Moonglade.Data.SqlServer/Configurations/PageConfiguration.cs index 816a59d0c..912eb2d21 100644 --- a/src/Moonglade.Data.SqlServer/Configurations/PageConfiguration.cs +++ b/src/Moonglade.Data.SqlServer/Configurations/PageConfiguration.cs @@ -11,6 +11,7 @@ public void Configure(EntityTypeBuilder builder) builder.Property(e => e.Id).ValueGeneratedNever(); builder.Property(e => e.Title).HasMaxLength(128); builder.Property(e => e.Slug).HasMaxLength(128); + builder.Property(e => e.CssId).HasMaxLength(64); builder.Property(e => e.MetaDescription).HasMaxLength(256); builder.Property(e => e.CreateTimeUtc).HasColumnType("datetime"); builder.Property(e => e.UpdateTimeUtc).HasColumnType("datetime"); diff --git a/src/Moonglade.Data/Entities/PageEntity.cs b/src/Moonglade.Data/Entities/PageEntity.cs index a219b1099..dd5e8158a 100644 --- a/src/Moonglade.Data/Entities/PageEntity.cs +++ b/src/Moonglade.Data/Entities/PageEntity.cs @@ -7,7 +7,7 @@ public class PageEntity public string Slug { get; set; } public string MetaDescription { get; set; } public string HtmlContent { get; set; } - public string CssContent { get; set; } + public string CssId { get; set; } public bool HideSidebar { get; set; } public bool IsPublished { get; set; } public DateTime CreateTimeUtc { get; set; } diff --git a/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs b/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs index 34dc97512..a0a8b7426 100644 --- a/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs +++ b/src/Moonglade.Data/Exporting/ExportPageDataCommand.cs @@ -22,7 +22,7 @@ public Task Handle(ExportPageDataCommand request, CancellationToke p.Slug, p.MetaDescription, p.HtmlContent, - p.CssContent, + p.CssId, p.HideSidebar, p.IsPublished, p.CreateTimeUtc, diff --git a/src/Moonglade.Data/Spec/StyleSheetSpec.cs b/src/Moonglade.Data/Spec/StyleSheetSpec.cs deleted file mode 100644 index 7f5c252cc..000000000 --- a/src/Moonglade.Data/Spec/StyleSheetSpec.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Moonglade.Data.Entities; -using Moonglade.Data.Infrastructure; - -namespace Moonglade.Data.Spec; - -public class StyleSheetByFriendlyNameSpec : BaseSpecification -{ - public StyleSheetByFriendlyNameSpec(string friendlyName) : base(p => p.FriendlyName == friendlyName) - { - } -} \ No newline at end of file diff --git a/src/Moonglade.Theme/GetStyleSheetQuery.cs b/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs similarity index 83% rename from src/Moonglade.Theme/GetStyleSheetQuery.cs rename to src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs index 6a57a7be7..c73d1b7a8 100644 --- a/src/Moonglade.Theme/GetStyleSheetQuery.cs +++ b/src/Moonglade.Theme/GetSiteThemeStyleSheetQuery.cs @@ -6,14 +6,14 @@ namespace Moonglade.Theme; -public record GetStyleSheetQuery(int Id) : IRequest; -public class GetStyleSheetQueryHandler : IRequestHandler +public record GetSiteThemeStyleSheetQuery(int Id) : IRequest; +public class GetStyleSheetQueryHandler : IRequestHandler { private readonly IRepository _repo; public GetStyleSheetQueryHandler(IRepository repo) => _repo = repo; - public async Task Handle(GetStyleSheetQuery request, CancellationToken ct) + public async Task Handle(GetSiteThemeStyleSheetQuery request, CancellationToken ct) { var theme = await _repo.GetAsync(request.Id, ct); if (null == theme) return null; diff --git a/src/Moonglade.Web/Controllers/ThemeController.cs b/src/Moonglade.Web/Controllers/ThemeController.cs index 2159d038e..1c010943a 100644 --- a/src/Moonglade.Web/Controllers/ThemeController.cs +++ b/src/Moonglade.Web/Controllers/ThemeController.cs @@ -40,7 +40,7 @@ public async Task Css() await _mediator.Send(new UpdateConfigurationCommand(kvp.Key, kvp.Value)); } - var data = await _mediator.Send(new GetStyleSheetQuery(_blogConfig.GeneralSettings.ThemeId)); + var data = await _mediator.Send(new GetSiteThemeStyleSheetQuery(_blogConfig.GeneralSettings.ThemeId)); return data; }); diff --git a/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs b/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs index 06f06e123..a5ba38424 100644 --- a/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs +++ b/src/Moonglade.Web/Middleware/StyleSheetMiddleware.cs @@ -1,6 +1,4 @@ -using System.Net; -using System.Text.RegularExpressions; -using System.Web; +using System.Web; using NUglify; namespace Moonglade.Web.Middleware; @@ -13,14 +11,14 @@ public class StyleSheetMiddleware public StyleSheetMiddleware(RequestDelegate next) => _next = next; - public async Task Invoke(HttpContext context, IBlogConfig blogConfig) + public async Task Invoke(HttpContext context, IBlogConfig blogConfig, IMediator mediator) { if (!context.Request.Path.ToString().ToLower().EndsWith(".css")) { await _next(context); return; } - + if (context.Request.Path == Options.DefaultPath) { if (!blogConfig.CustomStyleSheetSettings.EnableCustomCss) @@ -36,21 +34,26 @@ public async Task Invoke(HttpContext context, IBlogConfig blogConfig) { // Get query string value var qs = HttpUtility.ParseQueryString(context.Request.QueryString.Value!); - string slug = qs["slug"]; + var id = qs["id"]; - if (!string.IsNullOrWhiteSpace(slug)) + if (!string.IsNullOrWhiteSpace(id)) { - slug = slug.ToLower(); - - var slugRegex = "^(?!-)([a-zA-Z0-9-]){1,128}$"; - if (!Regex.IsMatch(slug, slugRegex, RegexOptions.Compiled, TimeSpan.FromSeconds(1))) + if (!Guid.TryParse(id, out var guid)) { context.Response.StatusCode = StatusCodes.Status404NotFound; } - - // TODO: Output blog page css - // Need a server side cache - // Need pattern validation + else + { + var css = await mediator.Send(new GetStyleSheetQuery(guid)); + if (css == null) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + + // TODO: May need a server side cache + await WriteStyleSheet(context, css.CssContent); + } } else { diff --git a/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs b/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs index aca059cb0..cbfc353cd 100644 --- a/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs +++ b/src/Moonglade.Web/Pages/Admin/EditPage.cshtml.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Moonglade.Core.PageFeature; +using Moonglade.Data.Entities; namespace Moonglade.Web.Pages.Admin; @@ -24,6 +25,12 @@ public async Task OnGetAsync(Guid? id) var page = await _mediator.Send(new GetPageByIdQuery(id.Value)); if (page is null) return NotFound(); + StyleSheetEntity css = null; + if (!string.IsNullOrWhiteSpace(page.CssId)) + { + css = await _mediator.Send(new GetStyleSheetQuery(Guid.Parse(page.CssId))); + } + PageId = page.Id; EditPageRequest = new() @@ -31,7 +38,7 @@ public async Task OnGetAsync(Guid? id) Title = page.Title, Slug = page.Slug, MetaDescription = page.MetaDescription, - CssContent = page.CssContent, + CssContent = css?.CssContent, RawHtmlContent = page.RawHtmlContent, HideSidebar = page.HideSidebar, IsPublished = page.IsPublished diff --git a/src/Moonglade.Web/Pages/BlogPage.cshtml b/src/Moonglade.Web/Pages/BlogPage.cshtml index dc97107a1..21fa754ed 100644 --- a/src/Moonglade.Web/Pages/BlogPage.cshtml +++ b/src/Moonglade.Web/Pages/BlogPage.cshtml @@ -1,32 +1,16 @@ @page "/page/{slug:regex(^(?!-)([a-zA-Z0-9-]){{1,128}}$)}" @model Moonglade.Web.Pages.BlogPageModel -@using NUglify @{ ViewBag.TitlePrefix = Model.BlogPage.Title; ViewBag.HideSideBar = Model.BlogPage.HideSidebar; ViewBag.DisableLightSwitch = true; - - string css = null; - if (!string.IsNullOrWhiteSpace(Model.BlogPage.CssContent)) - { - var uglifyResult = Uglify.Css(Model.BlogPage.CssContent); - if (!uglifyResult.HasErrors) - { - css = uglifyResult.Code; - } - } - } @section head{ - - - @if (!string.IsNullOrWhiteSpace(css)) + @if (!string.IsNullOrWhiteSpace(Model.BlogPage.CssId)) { - + } } diff --git a/src/Moonglade.Web/Pages/BlogPage.cshtml.cs b/src/Moonglade.Web/Pages/BlogPage.cshtml.cs index f676f0df0..ba2e0b068 100644 --- a/src/Moonglade.Web/Pages/BlogPage.cshtml.cs +++ b/src/Moonglade.Web/Pages/BlogPage.cshtml.cs @@ -25,7 +25,7 @@ public async Task OnGetAsync(string slug) var page = await _cache.GetOrCreateAsync(BlogCachePartition.Page.ToString(), slug.ToLower(), async entry => { - entry.SlidingExpiration = TimeSpan.FromMinutes(int.Parse(_configuration["CacheSlidingExpirationMinutes:Page"])); + entry.SlidingExpiration = TimeSpan.FromMinutes(int.Parse(_configuration["CacheSlidingExpirationMinutes:Page"]!)); var p = await _mediator.Send(new GetPageBySlugQuery(slug)); return p; diff --git a/src/Moonglade.Web/Pages/PagePreview.cshtml b/src/Moonglade.Web/Pages/PagePreview.cshtml index f336e1156..9ac520035 100644 --- a/src/Moonglade.Web/Pages/PagePreview.cshtml +++ b/src/Moonglade.Web/Pages/PagePreview.cshtml @@ -1,32 +1,18 @@ @page "/page/preview/{pageId:guid}" @model Moonglade.Web.Pages.PagePreviewModel -@using NUglify @{ ViewBag.TitlePrefix = Model.BlogPage.Title; ViewBag.HideSideBar = Model.BlogPage.HideSidebar; ViewBag.DisableLightSwitch = true; - - string css = null; - if (!string.IsNullOrWhiteSpace(Model.BlogPage.CssContent)) - { - var uglifyResult = Uglify.Css(Model.BlogPage.CssContent); - if (!uglifyResult.HasErrors) - { - css = uglifyResult.Code; - } - } } @section head{ - @if (!string.IsNullOrWhiteSpace(css)) + @if (!string.IsNullOrWhiteSpace(Model.BlogPage.CssId)) { - + } } - @section metadescription{ }