Skip to content

feat(314): add RTBlazorfied rich text editor + HtmlSanitizer backend#315

Merged
mpaulosky merged 15 commits into
devfrom
squad/314-html-sanitizer-backend
May 12, 2026
Merged

feat(314): add RTBlazorfied rich text editor + HtmlSanitizer backend#315
mpaulosky merged 15 commits into
devfrom
squad/314-html-sanitizer-backend

Conversation

@mpaulosky

@mpaulosky mpaulosky commented May 12, 2026

Copy link
Copy Markdown
Owner

Closes #314

Working as Legolas (Frontend Developer) + Sam (Backend Developer)

Summary

This PR completes Issue #314 end-to-end:

  1. Frontend (Legolas): Replaces <InputTextArea> with RTBlazorfied v2.0.20 rich text editor on Create and Edit blog post pages
  2. Backend (Sam): Adds server-side HTML sanitization via IHtmlSanitizer in the Create and Edit handlers

Note: Blazored.TextEditor referenced in the original plan does not exist on NuGet. RTBlazorfied was chosen instead: 52K+ downloads, supports @bind-Value, actively maintained (last update May 2026), shadow DOM isolated.

Changes

Frontend — RTBlazorfied Rich Text Editor (Legolas)

  • Directory.Packages.props: Added RTBlazorfied v2.0.20
  • src/Web/Web.csproj: Added <PackageReference Include="RTBlazorfied" />
  • src/Web/Components/App.razor: Added RTBlazorfied JS script tag
  • src/Web/Features/_Imports.razor + Components/_Imports.razor: Added @using RichTextBlazorfied
  • src/Web/Features/BlogPosts/Create/Create.razor: Replaced <InputTextArea><RTBlazorfied @bind-Value="_model.Content" Height="400px" />
  • src/Web/Features/BlogPosts/Edit/Edit.razor: Same replacement
  • tests/Web.Tests.Bunit/Features/RichTextEditorTests.cs: New — 2 smoke tests
  • tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs: Updated for shadow DOM + ValueChanged + JSRuntimeMode.Loose
  • tests/Web.Tests.Bunit/Features/EditAclTests.cs: Added JSRuntimeMode.Loose

Backend — HtmlSanitizer (Sam)

  • CreateBlogPostHandler and EditBlogPostHandler: sanitize Content with IHtmlSanitizer
  • HtmlSanitizer 9.1.923-beta + pinned AngleSharp 1.4.0
  • IHtmlSanitizer registered as singleton in Program.cs
  • Updated handler tests + new HtmlSanitizerBehaviorTests (7 tests)

Test results

All tests pass ✅: 94 bUnit + 165 Web.Tests + 42 Domain.Tests

Technical notes (RTBlazorfied)

  • In C# files always use fully-qualified RichTextBlazorfied.RTBlazorfied (namespace ambiguity with assembly root namespace)
  • Shadow DOM: bound content does not appear in cut.Markup
  • bUnit: JSRuntimeMode.Loose required in any test class rendering Create/Edit pages

⚠️ This task was flagged as "needs review" — please have a squad member review before merging.

Boromir and others added 13 commits May 11, 2026 10:45
…ession

- Merge decision inbox into .squad/decisions.md (decisions 30-31)
- Decision 30: Dark Mode Base Text Colours Must Contrast Against Dark Backgrounds (Legolas)
- Decision 31: CSS Visual Regressions and Test/Visual Finding Arbitration (Aragorn)
- Update Legolas history: UI regression review findings (Sprint 16 fan-out)
- Update Gimli history: Blazor UI regression testing (Issue #292 coverage + visual findings)
- Update Aragorn history: Issue triage (#292) and PR #295 arbitration workflow

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…und 1)

- Closed duplicate issue #294, merged PR #295 with all 19 CI checks green
- Sprint-stamped issues #293 and #296 with title normalization
- Removed go:needs-research from #293 (sufficient context), kept for #296 (Auth investigation needed)
- Decision #32: PR self-authored approval pattern and Sprint triage workflow

Co-authored-by: Boromir <lead-organizer@squad.local>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace old non-existent project ID (PVT_kwHOA5k0b84BVFTy) with new one (PVT_kwHOA5k0b84BXZpa)
- Update STATUS_FIELD_ID to match new project (PVTSSF_lAHOA5k0b84BXZpazhSmuGY)
- Map old status options to available field options (Backlog/InSprint: f75ad846, InReview: 47fc9ee4, Done: 98236657)
- Use GH_PROJECT_TOKEN secret for GraphQL mutations (fallback to GITHUB_TOKEN)
- Fixes issue where project board sync was failing silently due to missing project

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace token fallback with explicit GH_PROJECT_TOKEN requirement
- Ensures workflows have required 'project' scope for user-owned project boards
- Fixes 'Resource not accessible by integration' errors

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Kept HEAD version with correct option IDs from board fix:
- IN_SPRINT_OPTION_ID: f75ad846
- IN_REVIEW_OPTION_ID: 47fc9ee4

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t test

- Edit.razor: null-value branch now navigates to /blog instead of
  leaving _model null (which caused infinite 'Loading...' state)
- EditAclTests: added EditRedirectsToBlogWhenPostNotFound covering
  the Result.Ok(null) path

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#309, #310)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ox, seed schema)

Resolves merge conflict in MongoDbResourceBuilderExtensions.cs — indentation-only divergence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add HtmlSanitizer 9.0.892 package to Directory.Packages.props and Web.csproj
- Register IHtmlSanitizer as singleton in Program.cs
- Sanitize Content field in CreateBlogPostHandler before persisting
- Sanitize Content field in EditBlogPostHandler before persisting
- Log audit warning (LoggerMessage pattern) when unsafe HTML is stripped
- Return meaningful validation error if sanitized content is empty/whitespace
- Update existing handler tests to supply IHtmlSanitizer and NullLogger

Closes #314

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…r behavior tests

- Upgrade HtmlSanitizer 9.0.892 → 9.1.923-beta (uses AngleSharp 1.4.0, compatible with bunit 2.7.2)
- Pin AngleSharp 1.4.0 and AngleSharp.Css 1.0.0-beta.216 as transitive resolution
- Bump SharpCompress transitive pin to 1.0.0
- dotnet format: Razor and bunit formatting fixes
- Add HtmlSanitizerBehaviorTests (7 new tests covering sanitizer code paths)
- Fix MD013/MD032/MD012 markdown lint issues in aragorn/history.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 12, 2026 04:02
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ PR Added to Squad Triage Queue

This PR has been labeled with squad and added to the triage queue.

Next steps:

  • The squad Lead will review and assign to an appropriate team member
  • A squad:member label will be added after triage

If you know which squad member should handle this, you can add the appropriate squad:member label yourself.

@github-actions github-actions Bot added the squad Squad triage inbox — Lead will assign to a member label May 12, 2026
@mpaulosky mpaulosky changed the title feat(314): add HtmlSanitizer to CreateBlogPost and EditBlogPost handlers feat(314): add RTBlazorfied rich text editor + HtmlSanitizer backend May 12, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds server-side HTML sanitization for blog post rich-text content during create/edit operations, and wires in a rich text editor UI component plus targeted tests to validate the behavior.

Changes:

  • Sanitize blog post Content in CreateBlogPostHandler and EditBlogPostHandler, failing requests when content becomes empty after sanitization.
  • Add/central-pin HtmlSanitizer and RTBlazorfied packages and register IHtmlSanitizer in DI.
  • Update and expand unit + bUnit tests to cover sanitization behavior and rich text editor rendering/interaction.

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/Web.Tests/Handlers/HtmlSanitizerBehaviorTests.cs New tests validating handler behavior when sanitization changes/empties content + basic real-sanitizer integration assertions.
tests/Web.Tests/Handlers/EditBlogPostHandlerTests.cs Updates unit tests to inject/mimic sanitizer + logger for the updated handler constructor.
tests/Web.Tests/Handlers/CreateBlogPostHandlerTests.cs Updates unit tests to inject/mimic sanitizer + logger for the updated handler constructor.
tests/Web.Tests.Bunit/Features/RichTextEditorTests.cs New bUnit tests asserting the rich text editor component renders on Create/Edit pages.
tests/Web.Tests.Bunit/Features/EditAclTests.cs Sets JSInterop to loose mode for tests impacted by new JS-based editor.
tests/Web.Tests.Bunit/Components/RazorSmokeTests.cs Updates smoke tests to account for the editor component + loosens JSInterop mode; adjusts create submit tests to be async.
src/Web/Web.csproj Adds package references for HtmlSanitizer + RTBlazorfied to the web project.
src/Web/Program.cs Registers IHtmlSanitizer in DI as a singleton.
src/Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs Sanitizes edited content before update; logs when sanitization changes content; guards against empty-after-sanitization.
src/Web/Features/BlogPosts/Edit/Edit.razor Replaces textarea with RTBlazorfied rich text editor component.
src/Web/Features/BlogPosts/Create/CreateBlogPostHandler.cs Sanitizes created content before persistence; logs when sanitization changes content; guards against empty-after-sanitization.
src/Web/Features/BlogPosts/Create/Create.razor Replaces textarea with RTBlazorfied rich text editor component.
src/Web/Features/_Imports.razor Adds @using RichTextBlazorfied for feature components.
src/Web/Components/App.razor Includes RTBlazorfied static JS asset.
src/Web/Components/_Imports.razor Adds @using RichTextBlazorfied for shared components.
src/AppHost/MongoDbResourceBuilderExtensions.cs Minor formatting fix in seed document initializer.
scripts/inspect-dll/inspect.csx Adds a small script to list exported types from a DLL.
Directory.Packages.props Central version pins for HtmlSanitizer + AngleSharp(+Css) + RTBlazorfied; updates SharpCompress pin.
.squad/agents/gimli/history.md Adds historical test run notes (docs/logging).
.squad/agents/aragorn/history.md Adds/rewraps historical review notes (docs/logging).

Comment on lines +25 to +36
[LoggerMessage(Level = LogLevel.Warning, Message = "HTML sanitized on CreateBlogPost — unsafe markup was removed. Title: {Title}")]
private static partial void LogHtmlSanitized(ILogger logger, string title);

public async Task<Result<Guid>> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
{
try
{
var post = BlogPost.Create(request.Title, request.Content, request.Author);
var sanitizedContent = sanitizer.Sanitize(request.Content);
if (sanitizedContent != request.Content)
{
LogHtmlSanitized(logger, request.Title);
}
Comment thread Directory.Packages.props Outdated
@@ -23,7 +28,7 @@
<PackageVersion Include="MongoDB.Driver" Version="3.8.0" />
<PackageVersion Include="MongoDB.EntityFrameworkCore" Version="10.0.1" />
<!-- Transitive pin: MongoDB.Driver 3.6.x currently resolves vulnerable SharpCompress 0.30.1 -->
Comment on lines +27 to +43
[LoggerMessage(Level = LogLevel.Warning, Message = "HTML sanitized on EditBlogPost — unsafe markup was removed. PostId: {PostId}")]
private static partial void LogHtmlSanitized(ILogger logger, Guid postId);

public async Task<Result> Handle(EditBlogPostCommand request, CancellationToken cancellationToken)
{
try
{
var sanitizedContent = sanitizer.Sanitize(request.Content);
if (sanitizedContent != request.Content)
{
LogHtmlSanitized(logger, request.Id);
}

if (string.IsNullOrWhiteSpace(sanitizedContent))
{
return Result.Fail("Content is empty after sanitization. Please provide valid content.");
}
@github-actions

github-actions Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

Test Results Summary

378 tests  +9   377 ✅ +9   21s ⏱️ -1s
  6 suites ±0     1 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit d13064b. ± Comparison against base commit 8aef7ee.

♻️ This comment has been updated with latest results.

@codecov

codecov Bot commented May 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.01%. Comparing base (8aef7ee) to head (d13064b).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #315      +/-   ##
==========================================
+ Coverage   83.84%   84.01%   +0.17%     
==========================================
  Files          47       47              
  Lines        1300     1314      +14     
  Branches      157      161       +4     
==========================================
+ Hits         1090     1104      +14     
  Misses        148      148              
  Partials       62       62              
Files with missing lines Coverage Δ
src/AppHost/MongoDbResourceBuilderExtensions.cs 95.10% <100.00%> (ø)
src/Web/Features/BlogPosts/Create/Create.razor 79.41% <ø> (ø)
...Features/BlogPosts/Create/CreateBlogPostHandler.cs 100.00% <100.00%> (ø)
src/Web/Features/BlogPosts/Edit/Edit.razor 91.37% <ø> (ø)
...Web/Features/BlogPosts/Edit/EditBlogPostHandler.cs 96.00% <100.00%> (+0.65%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mpaulosky

Copy link
Copy Markdown
Owner Author

⚠️ APPROVED WITH NOTES — Architecture Gate PR #315

Reviewed by: Aragorn (Lead / Architect)


RTBlazorfied Substitution: ✅ Accepted

Blazored.TextEditor does not exist on NuGet — the substitution was forced, not optional. RTBlazorfied v2.0.20 is architecturally superior to the original plan: it supports @bind-Value directly, eliminating the GetHTML() workaround I had documented in Decision #5. 52K downloads, MIT license, actively maintained. Accepted.


Security Gate: ✅ PASSED (non-negotiable)

  • CreateBlogPostHandler: sanitizer.Sanitize(request.Content) called before BlogPost.Create(). No unsanitized content reaches MongoDB.
  • EditBlogPostHandler: sanitizer.Sanitize(request.Content) is the first operation, before repo.GetByIdAsync. Empty-after-sanitization guard returns Result.Fail without repo write.
  • IHtmlSanitizer registered as singleton (thread-safe). Correct.

Component Integration Gate: ✅ PASSED

  • @bind-Value="_model.Content" used in both Create.razor and Edit.razor. Two-way binding correct.
  • Content remains string throughout. No type break.
  • Validation flows through EditForm / DataAnnotationsValidator / [Required].

Test Coverage: ✅ PASSED

7 sanitizer behavior tests including XSS attack vectors (script injection, onclick stripping, empty-after-sanitization with no repo write). 2 bUnit smoke tests. All CI jobs green — CodeQL, all test suites, Codecov pass.


Copilot Review Comments (style only, no blockers)

  1. Log message vs. PR description mismatch — implementation is correct (no raw HTML in logs is good security hygiene). Update PR description to match.
  2. Stale SharpCompress comment in Directory.Packages.props — fix before merge.

Minor Notes for Next Cycle

  1. HtmlSanitizer 9.1.923-beta: upgrade to stable 9.1.x when published.
  2. .squad/ history files in PR diff — exclude on future squad branches.

Verdict: ⚠️ APPROVED WITH NOTES

RTBlazorfied is acceptable. Architecture is intact and improved. Fix the stale SharpCompress comment in Directory.Packages.props before squash-merging. Decision file updated at .squad/decisions/inbox/aragorn-rich-text-editor-plan.md. Coordinator may proceed once that one-line comment is corrected.

Boromir and others added 2 commits May 12, 2026 05:16
)

Merged decision inbox entries:
- Decision 32: OnParametersSetAsync state reset pattern (Aragorn)
- Decision 33: Cached state field reset (Aragorn)
- Decision 34: UserIdClaimsHelper recommendation (Aragorn)
- Decision 35: Rich Text Editor architectural plan (Aragorn)
- Decision 36: bUnit 2.x re-render API (Gimli)
- Decision 37: Display state reset pattern (Legolas)
- Decision 38: RTBlazorfied editor selection (Legolas)
- Decision 39: Loading-state reset + bUnit API (Sam)

Also updated Directory.Packages.props:
- MongoDB.Driver: 3.8.0 → 3.8.1
- Fixed stale SharpCompress transitive dependency comment

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mpaulosky
mpaulosky merged commit 8fa725d into dev May 12, 2026
19 checks passed
@mpaulosky
mpaulosky deleted the squad/314-html-sanitizer-backend branch May 12, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squad Squad triage inbox — Lead will assign to a member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Rich text editor component

2 participants