fix: register DOMPurify hooks once, abort featured upload, fix double drop

- Register DOMPurify hooks at module init (not per sanitizeHtml call)
- Always set iframe allow attribute (not conditional on existing)
- Add AbortController to featured image upload for unmount cleanup
- Check defaultPrevented in React onDrop to prevent double upload with TipTap
- Remove explicit Content-Type header on upload (axios auto-sets boundary)
This commit is contained in:
Fringg
2026-03-23 12:43:47 +03:00
parent f788f1034c
commit 5c0eb129f4
3 changed files with 71 additions and 66 deletions

View File

@@ -64,7 +64,7 @@ export const newsApi = {
const response = await apiClient.post<NewsMediaUploadResponse>( const response = await apiClient.post<NewsMediaUploadResponse>(
'/cabinet/admin/news/media/upload', '/cabinet/admin/news/media/upload',
formData, formData,
{ headers: { 'Content-Type': 'multipart/form-data' }, signal }, { signal },
); );
return response.data; return response.data;
}, },

View File

@@ -382,16 +382,21 @@ export default function AdminNewsCreate() {
return; return;
} }
const controller = new AbortController();
activeUploadsRef.current.add(controller);
setIsFeaturedImageUploading(true); setIsFeaturedImageUploading(true);
try { try {
const result = await newsApi.uploadMedia(file); const result = await newsApi.uploadMedia(file, controller.signal);
if (controller.signal.aborted) return;
setFeaturedImageUrl(result.url); setFeaturedImageUrl(result.url);
haptic.success(); haptic.success();
} catch { } catch {
if (controller.signal.aborted) return;
haptic.error(); haptic.error();
setSaveError(t('news.admin.uploadError')); setSaveError(t('news.admin.uploadError'));
} finally { } finally {
activeUploadsRef.current.delete(controller);
setIsFeaturedImageUploading(false); setIsFeaturedImageUploading(false);
} }
}, },
@@ -410,8 +415,10 @@ export default function AdminNewsCreate() {
const handleEditorDrop = useCallback( const handleEditorDrop = useCallback(
(e: React.DragEvent) => { (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false); setIsDragging(false);
// TipTap's handleDrop already handled media files dropped on the editor content
if (e.defaultPrevented) return;
e.preventDefault();
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
if (file) handleMediaUpload(file); if (file) handleMediaUpload(file);
}, },

View File

@@ -170,8 +170,8 @@ const SANITIZE_CONFIG = {
*/ */
const articlePurify = DOMPurify(window); const articlePurify = DOMPurify(window);
function sanitizeHtml(html: string): string { // Register sanitization hooks once at module init.
articlePurify.removeAllHooks(); // All hooks are stateless and idempotent — no need to add/remove per call.
// Hook: strip iframes with disallowed src // Hook: strip iframes with disallowed src
articlePurify.addHook('afterSanitizeAttributes', (node) => { articlePurify.addHook('afterSanitizeAttributes', (node) => {
@@ -184,10 +184,9 @@ function sanitizeHtml(html: string): string {
// Force sandbox — allow-scripts + allow-same-origin needed for YouTube/Vimeo // Force sandbox — allow-scripts + allow-same-origin needed for YouTube/Vimeo
// (cross-origin, so sandbox escape via frameElement is not possible) // (cross-origin, so sandbox escape via frameElement is not possible)
node.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-presentation'); node.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-presentation');
if (node.hasAttribute('allow')) { // Always set allow — restricts permissions even if original had none
node.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture'); node.setAttribute('allow', 'autoplay; encrypted-media; picture-in-picture');
} }
}
}); });
// Hook: validate <video> src — only allow http/https (block javascript:, data:, etc.) // Hook: validate <video> src — only allow http/https (block javascript:, data:, etc.)
@@ -231,9 +230,8 @@ function sanitizeHtml(html: string): string {
} }
}); });
const result = articlePurify.sanitize(html, SANITIZE_CONFIG); function sanitizeHtml(html: string): string {
articlePurify.removeAllHooks(); return articlePurify.sanitize(html, SANITIZE_CONFIG);
return result;
} }
/** /**