diff --git a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx index 89e37f1700746bd19abd7664192edadc835c6099..afc33ddb1acecc99c5a3e7be697c7c260f0de574 100644 --- a/apps/blog/app/[param]/[p2]/[permlink]/content.tsx +++ b/apps/blog/app/[param]/[p2]/[permlink]/content.tsx @@ -362,6 +362,7 @@ const PostContent = () => { mainPost={postData.depth === 0} body={crossPostData?.body ?? postData.body} author={postData.author} + permlink={postData.permlink} className={postClassName} /> diff --git a/apps/blog/features/post-rendering/comment-list-item.tsx b/apps/blog/features/post-rendering/comment-list-item.tsx index a085f6d381ebab5eaba00d93579a556b02e99f06..ac02bcb7b437cb52656ca5aec127d9cf415c6112 100644 --- a/apps/blog/features/post-rendering/comment-list-item.tsx +++ b/apps/blog/features/post-rendering/comment-list-item.tsx @@ -329,6 +329,7 @@ const CommentListItem = ({ diff --git a/apps/blog/features/post-rendering/rendererContainer.tsx b/apps/blog/features/post-rendering/rendererContainer.tsx index adf27b701a27879a8fc0a27564b29114b8932545..150ab1d20f1717758c25e106a83999132474e4f8 100644 --- a/apps/blog/features/post-rendering/rendererContainer.tsx +++ b/apps/blog/features/post-rendering/rendererContainer.tsx @@ -11,6 +11,7 @@ import { isUrlWhitelisted } from '@hive/ui/config/lists/phishing'; const RendererContainer = ({ body, author, + permlink, dataTestid, communityDescription, mainPost, @@ -18,6 +19,7 @@ const RendererContainer = ({ }: { body: string; author: string; + permlink?: string; dataTestid?: string; communityDescription?: boolean; className?: string; @@ -82,8 +84,11 @@ const RendererContainer = ({ }, [body, hiveRenderer]); const htmlBody = useMemo(() => { - if (body) return hiveRenderer.render(body); - }, [hiveRenderer, body]); + if (body) { + const postContext = author || permlink ? { author, permlink } : undefined; + return hiveRenderer.render(body, postContext); + } + }, [hiveRenderer, body, author, permlink]); return !htmlBody ? ( diff --git a/packages/renderer/src/index.ts b/packages/renderer/src/index.ts index 92643a7cf3d970bc678ef4af1ed6326e440ce7fa..4c067a90465a04a95b0b4c572a0e3435e6fd0ecd 100644 --- a/packages/renderer/src/index.ts +++ b/packages/renderer/src/index.ts @@ -8,6 +8,7 @@ export {TwitterPlugin} from './renderers/default/plugins/TwitterPlugin'; export {InstagramPlugin} from './renderers/default/plugins/InstagramPlugin'; export {TablePlugin} from './renderers/default/plugins/TablePlugin'; export type {RendererPlugin} from './renderers/default/plugins/RendererPlugin'; +export type {PostContext} from './renderers/default/sanitization/TagTransformingSanitizer'; export const HiveContentRenderer = { DefaultRenderer, diff --git a/packages/renderer/src/renderers/default/DefaultRenderer.ts b/packages/renderer/src/renderers/default/DefaultRenderer.ts index eb04654c093e23ef3cfbf8be1d4c59546a71ca4d..ae3e3b977ecad90da17b19e92efe2f8905d863ff 100644 --- a/packages/renderer/src/renderers/default/DefaultRenderer.ts +++ b/packages/renderer/src/renderers/default/DefaultRenderer.ts @@ -6,7 +6,7 @@ import {Localization, LocalizationOptions} from './Localization'; import type {RendererPlugin} from './plugins/RendererPlugin'; import remarkableSpoiler from './plugins/SpoilerPlugin'; import {PreliminarySanitizer} from './sanitization/PreliminarySanitizer'; -import {TagTransformingSanitizer} from './sanitization/TagTransformingSanitizer'; +import {TagTransformingSanitizer, PostContext} from './sanitization/TagTransformingSanitizer'; /** * DefaultRenderer is a configurable HTML/Markdown renderer that provides: @@ -68,22 +68,24 @@ export class DefaultRenderer { /** * Renders the input text to HTML * @param input - Markdown or HTML text to render + * @param postContext - Optional context about the post (author/permlink) for logging * @returns Rendered and processed HTML * @throws Will throw if input is empty or invalid */ - public render(input: string): string { + public render(input: string, postContext?: PostContext): string { // Validate input ow(input, 'input', ow.string.nonEmpty); - return this.doRender(input); + return this.doRender(input, postContext); } /** * Renders the input text to HTML with a specific locale * @param text - Markdown or HTML text to render + * @param postContext - Optional context about the post for logging * @returns Rendered and processed HTML * @throws Will throw if input is empty or invalid */ - private doRender(text: string): string { + private doRender(text: string, postContext?: PostContext): string { // Pre-process with plugins text = this.runPluginPhase('preProcess', text); // Preliminary sanitization @@ -98,7 +100,7 @@ export class DefaultRenderer { // Parse the HTML and sanitize it text = this.domParser.parse(text).getParsedDocumentAsString(); // Check for script tags and other security issues - text = this.sanitize(text); + text = this.sanitize(text, postContext); // Check for security issues SecurityChecker.checkSecurity(text, {allowScriptTag: this.options.allowInsecureScriptTags}); // Embed assets and resize them @@ -183,6 +185,7 @@ export class DefaultRenderer { /** * Sanitizes the HTML text by removing potentially harmful content * @param text - The HTML text to sanitize + * @param postContext - Optional context about the post for logging * @returns Sanitized HTML text * @remarks * This method can be skipped if skipSanitization option is set to true. @@ -191,12 +194,12 @@ export class DefaultRenderer { * - Transform certain tags according to renderer options * - Apply security policies to links and embedded content */ - private sanitize(text: string): string { + private sanitize(text: string, postContext?: PostContext): string { if (this.options.skipSanitization) { return text; } - return this.tagTransformingSanitizer.sanitize(text); + return this.tagTransformingSanitizer.sanitize(text, postContext); } /** * Validates the renderer options diff --git a/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts b/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts index 2e7ec9b99e170a08e7b77e6503c0f6550fe8015a..6b66754473d5002559feb62cbf103a038c1fa94f 100644 --- a/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts +++ b/packages/renderer/src/renderers/default/sanitization/TagTransformingSanitizer.ts @@ -11,6 +11,7 @@ export class TagTransformingSanitizer { private options: TagsSanitizerOptions; private localization: LocalizationOptions; private sanitizationErrors: string[] = []; + private currentPostContext?: PostContext; public constructor(options: TagsSanitizerOptions, localization: LocalizationOptions) { this.validate(options); @@ -25,12 +26,22 @@ export class TagTransformingSanitizer { * Uses the sanitize-html library with custom configuration for tag transformation. * * @param text - The HTML content to sanitize + * @param postContext - Optional context about the post being rendered (for logging) * @returns A sanitized version of the HTML content with transformed tags and removed unsafe content */ - public sanitize(text: string): string { + public sanitize(text: string, postContext?: PostContext): string { + this.currentPostContext = postContext; return sanitize(text, this.generateSanitizeConfig()); } + private formatPostContext(): string { + if (!this.currentPostContext) return ''; + const { author, permlink } = this.currentPostContext; + if (author && permlink) return ` in @${author}/${permlink}`; + if (author) return ` by @${author}`; + return ''; + } + public getErrors(): string[] { return this.sanitizationErrors; } @@ -98,7 +109,7 @@ export class TagTransformingSanitizer { return iframeToBeReturned; } } - Log.log().warn('Blocked, did not match iframe "src" white list urls:', tagName, attributes); + Log.log().warn(`Blocked iframe (not whitelisted)${this.formatPostContext()}: src="${srcAtty || '(empty)'}"`); this.sanitizationErrors.push('Invalid iframe URL: ' + srcAtty); const retTag: sanitize.Tag = {tagName: 'div', text: `(Unsupported ${srcAtty})`, attribs: {}}; @@ -117,7 +128,7 @@ export class TagTransformingSanitizer { const {src, alt} = attribs; // eslint-disable-next-line security/detect-unsafe-regex if (!/^(https?:)?\/\//i.test(src)) { - Log.log().warn('Blocked, image tag src does not appear to be a url', tagName, attribs); + Log.log().warn(`Blocked image (invalid src)${this.formatPostContext()}: src="${src || '(empty)'}"`); this.sanitizationErrors.push('An image in this post did not save properly.'); const retTagOnNoUrl: sanitize.Tag = { tagName: 'img', @@ -228,3 +239,8 @@ export interface TagsSanitizerOptions { isLinkSafeFn: (url: string) => boolean; addExternalCssClassToMatchingLinksFn: (url: string) => boolean; } + +export interface PostContext { + author?: string; + permlink?: string; +}