Bugfixes
This commit is contained in:
@@ -201,14 +201,14 @@ public class TypesenseService {
|
|||||||
|
|
||||||
if (authorFilters != null && !authorFilters.isEmpty()) {
|
if (authorFilters != null && !authorFilters.isEmpty()) {
|
||||||
String authorFilter = authorFilters.stream()
|
String authorFilter = authorFilters.stream()
|
||||||
.map(author -> "authorName:=" + author)
|
.map(author -> "authorName:=" + escapeTypesenseValue(author))
|
||||||
.collect(Collectors.joining(" || "));
|
.collect(Collectors.joining(" || "));
|
||||||
filterConditions.add("(" + authorFilter + ")");
|
filterConditions.add("(" + authorFilter + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tagFilters != null && !tagFilters.isEmpty()) {
|
if (tagFilters != null && !tagFilters.isEmpty()) {
|
||||||
String tagFilter = tagFilters.stream()
|
String tagFilter = tagFilters.stream()
|
||||||
.map(tag -> "tagNames:=" + tag)
|
.map(tag -> "tagNames:=" + escapeTypesenseValue(tag))
|
||||||
.collect(Collectors.joining(" || "));
|
.collect(Collectors.joining(" || "));
|
||||||
filterConditions.add("(" + tagFilter + ")");
|
filterConditions.add("(" + tagFilter + ")");
|
||||||
}
|
}
|
||||||
@@ -890,4 +890,31 @@ public class TypesenseService {
|
|||||||
return Map.of("error", e.getMessage());
|
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 handleVisualContentChange = () => {
|
||||||
const div = visualDivRef.current;
|
const div = visualDivRef.current;
|
||||||
@@ -198,16 +186,20 @@ export default function RichTextEditor({
|
|||||||
|
|
||||||
// Split plain text into paragraphs and insert as HTML
|
// Split plain text into paragraphs and insert as HTML
|
||||||
const paragraphs = plainText.split('\n\n').filter(p => p.trim());
|
const paragraphs = plainText.split('\n\n').filter(p => p.trim());
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
paragraphs.forEach((paragraph, index) => {
|
paragraphs.forEach((paragraph, index) => {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
// Add some spacing between paragraphs
|
// Add some spacing between paragraphs
|
||||||
range.insertNode(document.createElement('br'));
|
fragment.appendChild(document.createElement('br'));
|
||||||
}
|
}
|
||||||
const p = document.createElement('p');
|
const p = document.createElement('p');
|
||||||
p.textContent = paragraph.replace(/\n/g, ' ');
|
p.textContent = paragraph.replace(/\n/g, ' ');
|
||||||
range.insertNode(p);
|
fragment.appendChild(p);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
range.insertNode(fragment);
|
||||||
|
|
||||||
range.collapse(false);
|
range.collapse(false);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(range);
|
selection.addRange(range);
|
||||||
|
|||||||
@@ -12,6 +12,38 @@ interface SanitizationConfig {
|
|||||||
let cachedConfig: SanitizationConfig | null = null;
|
let cachedConfig: SanitizationConfig | null = null;
|
||||||
let configPromise: Promise<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
|
* Fetch sanitization configuration from backend
|
||||||
*/
|
*/
|
||||||
@@ -109,6 +141,23 @@ function createDOMPurifyConfig(config: SanitizationConfig) {
|
|||||||
ALLOW_DATA_ATTR: false,
|
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;
|
return domPurifyConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,41 +212,46 @@ export function sanitizeHtmlSync(html: string): string {
|
|||||||
|
|
||||||
// Use comprehensive fallback configuration that preserves formatting
|
// Use comprehensive fallback configuration that preserves formatting
|
||||||
console.log('Using fallback sanitization configuration with formatting support');
|
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 = {
|
const fallbackConfig: DOMPurify.Config = {
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
// Basic block elements
|
'p', 'br', 'div', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
'p', 'br', 'div', 'span', 'section', 'article',
|
|
||||||
// Headers
|
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
||||||
// Text formatting
|
|
||||||
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
|
'b', 'strong', 'i', 'em', 'u', 's', 'strike', 'del', 'ins',
|
||||||
'sup', 'sub', 'small', 'big', 'mark', 'abbr', 'dfn',
|
'sup', 'sub', 'small', 'big', 'mark', 'pre', 'code', 'kbd', 'samp', 'var',
|
||||||
// Code and preformatted
|
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a',
|
||||||
'pre', 'code', 'kbd', 'samp', 'var', 'tt',
|
|
||||||
// Lists
|
|
||||||
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
||||||
// Links (but href will be removed by backend config)
|
|
||||||
'a',
|
|
||||||
// Tables
|
|
||||||
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
|
'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',
|
||||||
// Quotes and misc
|
'blockquote', 'cite', 'q', 'hr', 'details', 'summary'
|
||||||
'blockquote', 'cite', 'q', 'hr', 'details', 'summary',
|
|
||||||
// Common website elements that might have formatting
|
|
||||||
'font', 'center'
|
|
||||||
],
|
],
|
||||||
ALLOWED_ATTR: [
|
ALLOWED_ATTR: [
|
||||||
'class', 'style', 'colspan', 'rowspan', 'align', 'valign',
|
'class', 'style', 'colspan', 'rowspan'
|
||||||
// Font attributes (though deprecated, websites still use them)
|
|
||||||
'color', 'size', 'face'
|
|
||||||
],
|
],
|
||||||
ALLOW_UNKNOWN_PROTOCOLS: false,
|
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||||
SANITIZE_DOM: true,
|
SANITIZE_DOM: true,
|
||||||
KEEP_CONTENT: true,
|
KEEP_CONTENT: true,
|
||||||
ALLOW_DATA_ATTR: false,
|
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();
|
return DOMPurify.sanitize(html, fallbackConfig as any).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user