Compare commits
2 Commits
f2001e0d0c
...
030aac7846
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
030aac7846 | ||
|
|
5a48ebcfeb |
@@ -42,6 +42,7 @@ public class SecurityConfig {
|
|||||||
// Public endpoints
|
// Public endpoints
|
||||||
.requestMatchers("/api/auth/**").permitAll()
|
.requestMatchers("/api/auth/**").permitAll()
|
||||||
.requestMatchers("/api/files/images/**").permitAll() // Public image serving
|
.requestMatchers("/api/files/images/**").permitAll() // Public image serving
|
||||||
|
.requestMatchers("/api/config/**").permitAll() // Public configuration endpoints
|
||||||
.requestMatchers("/actuator/health").permitAll()
|
.requestMatchers("/actuator/health").permitAll()
|
||||||
// All other API endpoints require authentication
|
// All other API endpoints require authentication
|
||||||
.requestMatchers("/api/**").authenticated()
|
.requestMatchers("/api/**").authenticated()
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.storycove.controller;
|
||||||
|
|
||||||
|
import com.storycove.dto.HtmlSanitizationConfigDto;
|
||||||
|
import com.storycove.service.HtmlSanitizationService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/config")
|
||||||
|
public class HtmlSanitizationController {
|
||||||
|
|
||||||
|
private final HtmlSanitizationService htmlSanitizationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public HtmlSanitizationController(HtmlSanitizationService htmlSanitizationService) {
|
||||||
|
this.htmlSanitizationService = htmlSanitizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the HTML sanitization configuration for frontend use
|
||||||
|
* This allows the frontend to use the same sanitization rules as the backend
|
||||||
|
*/
|
||||||
|
@GetMapping("/html-sanitization")
|
||||||
|
public ResponseEntity<HtmlSanitizationConfigDto> getHtmlSanitizationConfig() {
|
||||||
|
HtmlSanitizationConfigDto config = htmlSanitizationService.getConfiguration();
|
||||||
|
return ResponseEntity.ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.storycove.dto;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class HtmlSanitizationConfigDto {
|
||||||
|
private List<String> allowedTags;
|
||||||
|
private Map<String, List<String>> allowedAttributes;
|
||||||
|
private List<String> allowedCssProperties;
|
||||||
|
private Map<String, List<String>> removedAttributes;
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public HtmlSanitizationConfigDto() {}
|
||||||
|
|
||||||
|
public List<String> getAllowedTags() {
|
||||||
|
return allowedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedTags(List<String> allowedTags) {
|
||||||
|
this.allowedTags = allowedTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> getAllowedAttributes() {
|
||||||
|
return allowedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedAttributes(Map<String, List<String>> allowedAttributes) {
|
||||||
|
this.allowedAttributes = allowedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowedCssProperties() {
|
||||||
|
return allowedCssProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowedCssProperties(List<String> allowedCssProperties) {
|
||||||
|
this.allowedCssProperties = allowedCssProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<String>> getRemovedAttributes() {
|
||||||
|
return removedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemovedAttributes(Map<String, List<String>> removedAttributes) {
|
||||||
|
this.removedAttributes = removedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,113 @@
|
|||||||
package com.storycove.service;
|
package com.storycove.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.storycove.dto.HtmlSanitizationConfigDto;
|
||||||
import org.jsoup.Jsoup;
|
import org.jsoup.Jsoup;
|
||||||
import org.jsoup.safety.Safelist;
|
import org.jsoup.safety.Safelist;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class HtmlSanitizationService {
|
public class HtmlSanitizationService {
|
||||||
|
|
||||||
private final Safelist allowlist;
|
private static final Logger logger = LoggerFactory.getLogger(HtmlSanitizationService.class);
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private Safelist allowlist;
|
||||||
|
private HtmlSanitizationConfigDto config;
|
||||||
|
|
||||||
public HtmlSanitizationService() {
|
public HtmlSanitizationService(ObjectMapper objectMapper) {
|
||||||
// Create a custom allowlist for story content
|
this.objectMapper = objectMapper;
|
||||||
this.allowlist = Safelist.relaxed()
|
}
|
||||||
// Basic formatting
|
|
||||||
.addTags("p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6")
|
@PostConstruct
|
||||||
// Text formatting
|
public void initializeSanitization() {
|
||||||
.addTags("b", "strong", "i", "em", "u", "s", "strike", "del", "ins")
|
loadConfiguration();
|
||||||
.addTags("sup", "sub", "small", "big", "mark", "pre", "code")
|
createSafelist();
|
||||||
// Lists
|
}
|
||||||
.addTags("ul", "ol", "li", "dl", "dt", "dd")
|
|
||||||
// Links (but remove href for security)
|
private void loadConfiguration() {
|
||||||
.addTags("a").removeAttributes("a", "href", "target")
|
try {
|
||||||
// Tables
|
ClassPathResource resource = new ClassPathResource("html-sanitization-config.json");
|
||||||
.addTags("table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption")
|
try (InputStream inputStream = resource.getInputStream()) {
|
||||||
// Quotes
|
this.config = objectMapper.readValue(inputStream, HtmlSanitizationConfigDto.class);
|
||||||
.addTags("blockquote", "cite", "q")
|
logger.info("Loaded HTML sanitization configuration with {} allowed tags",
|
||||||
// Horizontal rule
|
config.getAllowedTags().size());
|
||||||
.addTags("hr")
|
}
|
||||||
// Allow basic styling attributes
|
} catch (IOException e) {
|
||||||
.addAttributes("p", "class", "style")
|
logger.error("Failed to load HTML sanitization configuration, using fallback", e);
|
||||||
.addAttributes("div", "class", "style")
|
createFallbackConfiguration();
|
||||||
.addAttributes("span", "class", "style")
|
}
|
||||||
.addAttributes("h1", "class", "style")
|
}
|
||||||
.addAttributes("h2", "class", "style")
|
|
||||||
.addAttributes("h3", "class", "style")
|
private void createFallbackConfiguration() {
|
||||||
.addAttributes("h4", "class", "style")
|
// Fallback configuration if JSON loading fails
|
||||||
.addAttributes("h5", "class", "style")
|
this.config = new HtmlSanitizationConfigDto();
|
||||||
.addAttributes("h6", "class", "style")
|
this.config.setAllowedTags(List.of(
|
||||||
// Allow limited CSS properties
|
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
.addProtocols("style", "color", "background-color", "font-size", "font-weight",
|
"b", "strong", "i", "em", "u", "s", "strike", "del", "ins",
|
||||||
"font-style", "text-align", "text-decoration", "margin", "padding");
|
"sup", "sub", "small", "big", "mark", "pre", "code",
|
||||||
|
"ul", "ol", "li", "dl", "dt", "dd", "a",
|
||||||
|
"table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
|
||||||
|
"blockquote", "cite", "q", "hr"
|
||||||
|
));
|
||||||
|
this.config.setAllowedCssProperties(List.of(
|
||||||
|
"color", "background-color", "font-size", "font-weight",
|
||||||
|
"font-style", "text-align", "text-decoration", "margin", "padding"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createSafelist() {
|
||||||
|
this.allowlist = new Safelist();
|
||||||
|
|
||||||
|
// Add allowed tags
|
||||||
|
if (config.getAllowedTags() != null) {
|
||||||
|
config.getAllowedTags().forEach(allowlist::addTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add allowed attributes
|
||||||
|
if (config.getAllowedAttributes() != null) {
|
||||||
|
for (Map.Entry<String, List<String>> entry : config.getAllowedAttributes().entrySet()) {
|
||||||
|
String tag = entry.getKey();
|
||||||
|
List<String> attributes = entry.getValue();
|
||||||
|
if (attributes != null) {
|
||||||
|
allowlist.addAttributes(tag, attributes.toArray(new String[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove specific attributes (like href from links for security)
|
||||||
|
if (config.getRemovedAttributes() != null) {
|
||||||
|
for (Map.Entry<String, List<String>> entry : config.getRemovedAttributes().entrySet()) {
|
||||||
|
String tag = entry.getKey();
|
||||||
|
List<String> attributes = entry.getValue();
|
||||||
|
if (attributes != null) {
|
||||||
|
attributes.forEach(attr -> allowlist.removeAttributes(tag, attr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add allowed CSS properties for style attributes
|
||||||
|
if (config.getAllowedCssProperties() != null) {
|
||||||
|
config.getAllowedCssProperties().forEach(property ->
|
||||||
|
allowlist.addProtocols("style", property));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Created Jsoup Safelist with configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current sanitization configuration for sharing with frontend
|
||||||
|
*/
|
||||||
|
public HtmlSanitizationConfigDto getConfiguration() {
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String sanitize(String html) {
|
public String sanitize(String html) {
|
||||||
|
|||||||
34
backend/src/main/resources/html-sanitization-config.json
Normal file
34
backend/src/main/resources/html-sanitization-config.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"allowedTags": [
|
||||||
|
"p", "br", "div", "span", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||||
|
"b", "strong", "i", "em", "u", "s", "strike", "del", "ins",
|
||||||
|
"sup", "sub", "small", "big", "mark", "pre", "code",
|
||||||
|
"ul", "ol", "li", "dl", "dt", "dd",
|
||||||
|
"a", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption",
|
||||||
|
"blockquote", "cite", "q", "hr"
|
||||||
|
],
|
||||||
|
"allowedAttributes": {
|
||||||
|
"p": ["class", "style"],
|
||||||
|
"div": ["class", "style"],
|
||||||
|
"span": ["class", "style"],
|
||||||
|
"h1": ["class", "style"],
|
||||||
|
"h2": ["class", "style"],
|
||||||
|
"h3": ["class", "style"],
|
||||||
|
"h4": ["class", "style"],
|
||||||
|
"h5": ["class", "style"],
|
||||||
|
"h6": ["class", "style"],
|
||||||
|
"a": ["class"],
|
||||||
|
"table": ["class"],
|
||||||
|
"td": ["class", "colspan", "rowspan"],
|
||||||
|
"th": ["class", "colspan", "rowspan"]
|
||||||
|
},
|
||||||
|
"allowedCssProperties": [
|
||||||
|
"color", "background-color", "font-size", "font-weight",
|
||||||
|
"font-style", "text-align", "text-decoration", "margin",
|
||||||
|
"padding", "text-indent", "line-height"
|
||||||
|
],
|
||||||
|
"removedAttributes": {
|
||||||
|
"a": ["href", "target"]
|
||||||
|
},
|
||||||
|
"description": "HTML sanitization configuration for StoryCove story content. This configuration is shared between frontend (DOMPurify) and backend (Jsoup) to ensure consistency."
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { Story } from '../../../types/api';
|
|||||||
import LoadingSpinner from '../../../components/ui/LoadingSpinner';
|
import LoadingSpinner from '../../../components/ui/LoadingSpinner';
|
||||||
import Button from '../../../components/ui/Button';
|
import Button from '../../../components/ui/Button';
|
||||||
import StoryRating from '../../../components/stories/StoryRating';
|
import StoryRating from '../../../components/stories/StoryRating';
|
||||||
import DOMPurify from 'dompurify';
|
import { sanitizeHtml, preloadSanitizationConfig } from '../../../lib/sanitization';
|
||||||
|
|
||||||
export default function StoryReadingPage() {
|
export default function StoryReadingPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -18,6 +18,7 @@ export default function StoryReadingPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [readingProgress, setReadingProgress] = useState(0);
|
const [readingProgress, setReadingProgress] = useState(0);
|
||||||
|
const [sanitizedContent, setSanitizedContent] = useState<string>('');
|
||||||
|
|
||||||
const storyId = params.id as string;
|
const storyId = params.id as string;
|
||||||
|
|
||||||
@@ -25,9 +26,19 @@ export default function StoryReadingPage() {
|
|||||||
const loadStory = async () => {
|
const loadStory = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const storyData = await storyApi.getStory(storyId);
|
|
||||||
|
// Preload sanitization config and load story in parallel
|
||||||
|
const [storyData] = await Promise.all([
|
||||||
|
storyApi.getStory(storyId),
|
||||||
|
preloadSanitizationConfig()
|
||||||
|
]);
|
||||||
|
|
||||||
setStory(storyData);
|
setStory(storyData);
|
||||||
|
|
||||||
|
// Sanitize story content
|
||||||
|
const sanitized = await sanitizeHtml(storyData.contentHtml || '');
|
||||||
|
setSanitizedContent(sanitized);
|
||||||
|
|
||||||
// Load series stories if part of a series
|
// Load series stories if part of a series
|
||||||
if (storyData.seriesId) {
|
if (storyData.seriesId) {
|
||||||
const seriesData = await seriesApi.getSeriesStories(storyData.seriesId);
|
const seriesData = await seriesApi.getSeriesStories(storyData.seriesId);
|
||||||
@@ -119,8 +130,6 @@ export default function StoryReadingPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitizedContent = DOMPurify.sanitize(story.contentHtml);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen theme-bg">
|
<div className="min-h-screen theme-bg">
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
|
|||||||
@@ -273,6 +273,20 @@ export const searchApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Configuration endpoints
|
||||||
|
export const configApi = {
|
||||||
|
getHtmlSanitizationConfig: async (): Promise<{
|
||||||
|
allowedTags: string[];
|
||||||
|
allowedAttributes: Record<string, string[]>;
|
||||||
|
allowedCssProperties: string[];
|
||||||
|
removedAttributes: Record<string, string[]>;
|
||||||
|
description: string;
|
||||||
|
}> => {
|
||||||
|
const response = await api.get('/config/html-sanitization');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Image utility
|
// Image utility
|
||||||
export const getImageUrl = (path: string): string => {
|
export const getImageUrl = (path: string): string => {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
|
|||||||
166
frontend/src/lib/sanitization.ts
Normal file
166
frontend/src/lib/sanitization.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
import { configApi } from './api';
|
||||||
|
|
||||||
|
interface SanitizationConfig {
|
||||||
|
allowedTags: string[];
|
||||||
|
allowedAttributes: Record<string, string[]>;
|
||||||
|
allowedCssProperties: string[];
|
||||||
|
removedAttributes: Record<string, string[]>;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedConfig: SanitizationConfig | null = null;
|
||||||
|
let configPromise: Promise<SanitizationConfig> | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch sanitization configuration from backend
|
||||||
|
*/
|
||||||
|
async function fetchSanitizationConfig(): Promise<SanitizationConfig> {
|
||||||
|
if (cachedConfig) {
|
||||||
|
return cachedConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPromise) {
|
||||||
|
return configPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
configPromise = configApi.getHtmlSanitizationConfig()
|
||||||
|
.then(config => {
|
||||||
|
cachedConfig = config;
|
||||||
|
configPromise = null;
|
||||||
|
return config;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Failed to fetch sanitization config, using fallback:', error);
|
||||||
|
configPromise = null;
|
||||||
|
// Return fallback configuration
|
||||||
|
const fallbackConfig: SanitizationConfig = {
|
||||||
|
allowedTags: [
|
||||||
|
'p', 'br', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
|
||||||
|
'sup', 'sub', 'small', 'big', 'mark', 'pre', 'code',
|
||||||
|
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a',
|
||||||
|
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption',
|
||||||
|
'blockquote', 'cite', 'q', 'hr'
|
||||||
|
],
|
||||||
|
allowedAttributes: {
|
||||||
|
'p': ['class', 'style'],
|
||||||
|
'div': ['class', 'style'],
|
||||||
|
'span': ['class', 'style'],
|
||||||
|
'h1': ['class', 'style'],
|
||||||
|
'h2': ['class', 'style'],
|
||||||
|
'h3': ['class', 'style'],
|
||||||
|
'h4': ['class', 'style'],
|
||||||
|
'h5': ['class', 'style'],
|
||||||
|
'h6': ['class', 'style'],
|
||||||
|
'a': ['class'],
|
||||||
|
'table': ['class'],
|
||||||
|
'td': ['class', 'colspan', 'rowspan'],
|
||||||
|
'th': ['class', 'colspan', 'rowspan']
|
||||||
|
},
|
||||||
|
allowedCssProperties: [
|
||||||
|
'color', 'background-color', 'font-size', 'font-weight',
|
||||||
|
'font-style', 'text-align', 'text-decoration', 'margin',
|
||||||
|
'padding', 'text-indent', 'line-height'
|
||||||
|
],
|
||||||
|
removedAttributes: {
|
||||||
|
'a': ['href', 'target']
|
||||||
|
},
|
||||||
|
description: 'Fallback sanitization configuration'
|
||||||
|
};
|
||||||
|
cachedConfig = fallbackConfig;
|
||||||
|
return fallbackConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
return configPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create DOMPurify configuration from backend sanitization config
|
||||||
|
*/
|
||||||
|
function createDOMPurifyConfig(config: SanitizationConfig) {
|
||||||
|
const allowedTags = config.allowedTags;
|
||||||
|
const allowedAttributes: Record<string, string[]> = { ...config.allowedAttributes };
|
||||||
|
|
||||||
|
// Remove attributes that should be stripped (like href from links)
|
||||||
|
if (config.removedAttributes) {
|
||||||
|
Object.keys(config.removedAttributes).forEach(tag => {
|
||||||
|
const attributesToRemove = config.removedAttributes[tag];
|
||||||
|
if (allowedAttributes[tag]) {
|
||||||
|
allowedAttributes[tag] = allowedAttributes[tag].filter(
|
||||||
|
attr => !attributesToRemove.includes(attr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ALLOWED_TAGS: allowedTags,
|
||||||
|
ALLOWED_ATTR: Object.keys(allowedAttributes).reduce((acc: string[], tag) => {
|
||||||
|
return [...acc, ...allowedAttributes[tag]];
|
||||||
|
}, []),
|
||||||
|
// Allow style attribute but sanitize CSS properties
|
||||||
|
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||||
|
SANITIZE_DOM: true,
|
||||||
|
KEEP_CONTENT: true,
|
||||||
|
// Custom hook to sanitize style attributes
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
} as DOMPurify.Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML content using shared configuration from backend
|
||||||
|
*/
|
||||||
|
export async function sanitizeHtml(html: string): Promise<string> {
|
||||||
|
if (!html || html.trim() === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = await fetchSanitizationConfig();
|
||||||
|
const domPurifyConfig = createDOMPurifyConfig(config);
|
||||||
|
|
||||||
|
// Configure DOMPurify with our settings
|
||||||
|
const cleanHtml = DOMPurify.sanitize(html, domPurifyConfig as any);
|
||||||
|
|
||||||
|
return cleanHtml.toString();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during HTML sanitization:', error);
|
||||||
|
// Fallback to basic DOMPurify sanitization
|
||||||
|
return DOMPurify.sanitize(html).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous sanitization using cached config (for cases where async is not possible)
|
||||||
|
* Falls back to basic DOMPurify if no config is cached
|
||||||
|
*/
|
||||||
|
export function sanitizeHtmlSync(html: string): string {
|
||||||
|
if (!html || html.trim() === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedConfig) {
|
||||||
|
const domPurifyConfig = createDOMPurifyConfig(cachedConfig);
|
||||||
|
return DOMPurify.sanitize(html, domPurifyConfig as any).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to basic DOMPurify
|
||||||
|
console.warn('No cached sanitization config available, using DOMPurify defaults');
|
||||||
|
return DOMPurify.sanitize(html).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload sanitization configuration (call early in app lifecycle)
|
||||||
|
*/
|
||||||
|
export function preloadSanitizationConfig(): Promise<SanitizationConfig> {
|
||||||
|
return fetchSanitizationConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear cached configuration (useful for testing or config updates)
|
||||||
|
*/
|
||||||
|
export function clearSanitizationCache(): void {
|
||||||
|
cachedConfig = null;
|
||||||
|
configPromise = null;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user