Bugfixes
This commit is contained in:
@@ -201,14 +201,14 @@ public class TypesenseService {
|
||||
|
||||
if (authorFilters != null && !authorFilters.isEmpty()) {
|
||||
String authorFilter = authorFilters.stream()
|
||||
.map(author -> "authorName:=" + author)
|
||||
.map(author -> "authorName:=" + escapeTypesenseValue(author))
|
||||
.collect(Collectors.joining(" || "));
|
||||
filterConditions.add("(" + authorFilter + ")");
|
||||
}
|
||||
|
||||
if (tagFilters != null && !tagFilters.isEmpty()) {
|
||||
String tagFilter = tagFilters.stream()
|
||||
.map(tag -> "tagNames:=" + tag)
|
||||
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
|
||||
.collect(Collectors.joining(" || "));
|
||||
filterConditions.add("(" + tagFilter + ")");
|
||||
}
|
||||
@@ -890,4 +890,31 @@ public class TypesenseService {
|
||||
return Map.of("error", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters in Typesense filter values.
|
||||
* Typesense requires certain characters to be escaped or quoted.
|
||||
*/
|
||||
private String escapeTypesenseValue(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// If the value contains spaces, special characters, or quotes, wrap it in backticks
|
||||
if (value.contains(" ") || value.contains("'") || value.contains("\"") ||
|
||||
value.contains("(") || value.contains(")") || value.contains("[") ||
|
||||
value.contains("]") || value.contains("{") || value.contains("}") ||
|
||||
value.contains(":") || value.contains("=") || value.contains("&") ||
|
||||
value.contains("|") || value.contains("!") || value.contains("<") ||
|
||||
value.contains(">") || value.contains("@") || value.contains("#") ||
|
||||
value.contains("$") || value.contains("%") || value.contains("^") ||
|
||||
value.contains("*") || value.contains("+") || value.contains("?") ||
|
||||
value.contains("\\") || value.contains("/") || value.contains("~") ||
|
||||
value.contains("`")) {
|
||||
// Escape backticks in the value and wrap with backticks
|
||||
return "`" + value.replace("`", "\\`") + "`";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -33,18 +33,6 @@ export default function RichTextEditor({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleVisualChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const plainText = e.target.value;
|
||||
// Convert plain text to basic HTML paragraphs
|
||||
const htmlContent = plainText
|
||||
.split('\n\n')
|
||||
.filter(paragraph => paragraph.trim())
|
||||
.map(paragraph => `<p>${paragraph.replace(/\n/g, '<br>')}</p>`)
|
||||
.join('\n');
|
||||
|
||||
onChange(htmlContent);
|
||||
setHtmlValue(htmlContent);
|
||||
};
|
||||
|
||||
const handleVisualContentChange = () => {
|
||||
const div = visualDivRef.current;
|
||||
@@ -198,16 +186,20 @@ export default function RichTextEditor({
|
||||
|
||||
// Split plain text into paragraphs and insert as HTML
|
||||
const paragraphs = plainText.split('\n\n').filter(p => p.trim());
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
paragraphs.forEach((paragraph, index) => {
|
||||
if (index > 0) {
|
||||
// Add some spacing between paragraphs
|
||||
range.insertNode(document.createElement('br'));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
}
|
||||
const p = document.createElement('p');
|
||||
p.textContent = paragraph.replace(/\n/g, ' ');
|
||||
range.insertNode(p);
|
||||
fragment.appendChild(p);
|
||||
});
|
||||
|
||||
range.insertNode(fragment);
|
||||
|
||||
range.collapse(false);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
@@ -12,6 +12,38 @@ interface SanitizationConfig {
|
||||
let cachedConfig: SanitizationConfig | null = null;
|
||||
let configPromise: Promise<SanitizationConfig> | null = null;
|
||||
|
||||
/**
|
||||
* Filter CSS properties in a style attribute value
|
||||
*/
|
||||
function filterCssProperties(styleValue: string, allowedProperties: string[]): string {
|
||||
// Parse CSS declarations
|
||||
const declarations = styleValue.split(';').map(decl => decl.trim()).filter(decl => decl);
|
||||
|
||||
const filteredDeclarations = declarations.filter(declaration => {
|
||||
const colonIndex = declaration.indexOf(':');
|
||||
if (colonIndex === -1) return false;
|
||||
|
||||
const property = declaration.substring(0, colonIndex).trim().toLowerCase();
|
||||
const isAllowed = allowedProperties.includes(property);
|
||||
|
||||
if (!isAllowed) {
|
||||
console.log(`CSS property "${property}" was filtered out (not in allowed list)`);
|
||||
}
|
||||
|
||||
return isAllowed;
|
||||
});
|
||||
|
||||
const result = filteredDeclarations.join('; ');
|
||||
|
||||
if (declarations.length !== filteredDeclarations.length) {
|
||||
console.log(`CSS filtering: ${declarations.length} -> ${filteredDeclarations.length} properties`);
|
||||
console.log('Original:', styleValue);
|
||||
console.log('Filtered:', result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sanitization configuration from backend
|
||||
*/
|
||||
@@ -109,6 +141,23 @@ function createDOMPurifyConfig(config: SanitizationConfig) {
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
// Clear any existing hooks and add CSS property filtering
|
||||
DOMPurify.removeAllHooks();
|
||||
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
||||
// Only process elements with style attributes
|
||||
if (node.hasAttribute && node.hasAttribute('style')) {
|
||||
const styleValue = node.getAttribute('style');
|
||||
if (styleValue) {
|
||||
const filteredStyle = filterCssProperties(styleValue, config.allowedCssProperties);
|
||||
if (filteredStyle) {
|
||||
node.setAttribute('style', filteredStyle);
|
||||
} else {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return domPurifyConfig;
|
||||
}
|
||||
|
||||
@@ -163,40 +212,45 @@ export function sanitizeHtmlSync(html: string): string {
|
||||
|
||||
// Use comprehensive fallback configuration that preserves formatting
|
||||
console.log('Using fallback sanitization configuration with formatting support');
|
||||
const fallbackAllowedCssProperties = [
|
||||
'color', 'font-size', 'font-weight',
|
||||
'font-style', 'text-align', 'text-decoration', 'margin',
|
||||
'padding', 'text-indent', 'line-height'
|
||||
];
|
||||
|
||||
const fallbackConfig: DOMPurify.Config = {
|
||||
ALLOWED_TAGS: [
|
||||
// Basic block elements
|
||||
'p', 'br', 'div', 'span', 'section', 'article',
|
||||
// Headers
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
// Text formatting
|
||||
'p', 'br', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
|
||||
'sup', 'sub', 'small', 'big', 'mark', 'abbr', 'dfn',
|
||||
// Code and preformatted
|
||||
'pre', 'code', 'kbd', 'samp', 'var', 'tt',
|
||||
// Lists
|
||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
||||
// Links (but href will be removed by backend config)
|
||||
'a',
|
||||
// Tables
|
||||
'sup', 'sub', 'small', 'big', 'mark', 'pre', 'code', 'kbd', 'samp', 'var',
|
||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a',
|
||||
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
|
||||
// Quotes and misc
|
||||
'blockquote', 'cite', 'q', 'hr', 'details', 'summary',
|
||||
// Common website elements that might have formatting
|
||||
'font', 'center'
|
||||
'blockquote', 'cite', 'q', 'hr', 'details', 'summary'
|
||||
],
|
||||
ALLOWED_ATTR: [
|
||||
'class', 'style', 'colspan', 'rowspan', 'align', 'valign',
|
||||
// Font attributes (though deprecated, websites still use them)
|
||||
'color', 'size', 'face'
|
||||
'class', 'style', 'colspan', 'rowspan'
|
||||
],
|
||||
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||
SANITIZE_DOM: true,
|
||||
KEEP_CONTENT: true,
|
||||
ALLOW_DATA_ATTR: false,
|
||||
// Don't strip style attributes completely - let them through for basic formatting
|
||||
FORBID_ATTR: [],
|
||||
};
|
||||
|
||||
// Clear hooks and add CSS property filtering for fallback config
|
||||
DOMPurify.removeAllHooks();
|
||||
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
|
||||
if (node.hasAttribute && node.hasAttribute('style')) {
|
||||
const styleValue = node.getAttribute('style');
|
||||
if (styleValue) {
|
||||
const filteredStyle = filterCssProperties(styleValue, fallbackAllowedCssProperties);
|
||||
if (filteredStyle) {
|
||||
node.setAttribute('style', filteredStyle);
|
||||
} else {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return DOMPurify.sanitize(html, fallbackConfig as any).toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user