- Received quotes: read from `quote`, `quoteUrl`, `quoteUri`, `_misskey_quote` or a FEP-e232 Link tag; the quoted post is fetched once; a quoteAuthorization stamp is verified field by field on the quoted author's origin; a consent quote without a stamp is pending; an older-key quote of a public post is shown; Delete of a stamp revokes. Counts and a `quote` notification follow the accepted state. The `quote-inline` fallback survives sanitising and is removed from content when the real quote is shown. - Personas quote through `quoted_status_id`: posts that state a quote policy get a QuoteRequest and stay pending until an Accept brings a stamp we can verify, then an Update adds quoteAuthorization; posts that state none are quoted the older way, without `quote`; another persona's posts cannot be quoted yet (we issue no stamps). Quoting posts are delivered to the quoted author too. - Mastodon API: Status.quote (with the quoted status one level deep), quotes_count, quote_approval from the remote policy, GET /api/v1/statuses/:id/quotes, `quote` notifications, and api_versions.mastodon = 7. Checked live: GoToSocial's author-only quote policy is respected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
81 lines
2.7 KiB
C#
81 lines
2.7 KiB
C#
using AngleSharp.Css.Dom;
|
|
using AngleSharp.Dom;
|
|
|
|
using Ganss.Xss;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace PrivaPub.Federation.Objects
|
|
{
|
|
public static partial class ContentSanitizer
|
|
{
|
|
static readonly HtmlSanitizer Sanitizer = Build();
|
|
|
|
public static string Html(string html) =>
|
|
string.IsNullOrWhiteSpace(html) ? string.Empty : Sanitizer.Sanitize(html).Trim();
|
|
|
|
static HtmlSanitizer Build()
|
|
{
|
|
var sanitizer = new HtmlSanitizer(new HtmlSanitizerOptions
|
|
{
|
|
AllowedTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"p", "br", "span", "a", "abbr", "del", "s", "pre", "blockquote", "code", "b", "strong", "u", "i", "em",
|
|
"sub", "sup", "ul", "ol", "li", "ruby", "rt", "rp", "h1", "h2", "h3", "h4", "h5", "h6"
|
|
},
|
|
AllowedAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"href", "rel", "class", "translate", "start", "reversed", "value", "title"
|
|
},
|
|
AllowedCssProperties = new HashSet<string>(),
|
|
AllowedAtRules = new HashSet<CssRuleType>(),
|
|
AllowedSchemes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"http", "https", "dat", "dweb", "ipfs", "ipns", "ssb", "gopher", "xmpp", "magnet", "gemini"
|
|
},
|
|
UriAttributes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "href" }
|
|
})
|
|
{
|
|
KeepChildNodes = true
|
|
};
|
|
foreach (var allowed in new[] { "mention", "hashtag", "ellipsis", "invisible", "quote-inline" })
|
|
sanitizer.AllowedClasses.Add(allowed);
|
|
sanitizer.RemovingCssClass += (_, e) => e.Cancel = MicroformatClass().IsMatch(e.CssClass);
|
|
sanitizer.RemovingTag += (_, e) =>
|
|
{
|
|
if (e.Tag.LocalName is "script" or "style" or "template" or "iframe" or "object" or "embed" or "noscript" or "svg" or "math")
|
|
e.Tag.InnerHtml = string.Empty;
|
|
};
|
|
sanitizer.PostProcessNode += (_, e) =>
|
|
{
|
|
if (e.Node is not IElement element)
|
|
return;
|
|
switch (element.LocalName)
|
|
{
|
|
case "a":
|
|
if (!SchemePrefix().IsMatch(element.GetAttribute("href") ?? string.Empty))
|
|
element.RemoveAttribute("href");
|
|
element.SetAttribute("rel", "nofollow noopener noreferrer");
|
|
element.SetAttribute("target", "_blank");
|
|
break;
|
|
case "h1" or "h2" or "h3" or "h4" or "h5" or "h6":
|
|
var paragraph = e.Document.CreateElement("p");
|
|
var strong = e.Document.CreateElement("strong");
|
|
while (element.FirstChild != default)
|
|
strong.AppendChild(element.FirstChild);
|
|
paragraph.AppendChild(strong);
|
|
e.ReplacementNodes.Add(paragraph);
|
|
break;
|
|
}
|
|
};
|
|
return sanitizer;
|
|
}
|
|
|
|
[GeneratedRegex("^[a-z][a-z0-9+.-]*:", RegexOptions.IgnoreCase)]
|
|
private static partial Regex SchemePrefix();
|
|
|
|
[GeneratedRegex("^(h|p|u|dt|e)-[a-z0-9-]+$")]
|
|
private static partial Regex MicroformatClass();
|
|
}
|
|
}
|