feat(webui): support document attachments with ingress safeguards (#4771)

* feat: support document attachments in webui

* fix(webui): normalize document attachment MIME

* refactor(webui): move attachment policy out of channel

* fix(webui): reject oversized attachments before send

* fix(webui): align Portuguese attachment errors

* refactor(webui): separate ingress and transport limits

* fix(webui): reject malformed attachment payloads
This commit is contained in:
chengyongru
2026-07-14 14:47:42 +08:00
committed by GitHub
parent b2759e8a6b
commit b7048cf76a
36 changed files with 1398 additions and 332 deletions
+12 -11
View File
@@ -1,12 +1,14 @@
import { useCallback, useRef, useState } from "react";
/** Extract image ``File``s from a paste / drop event.
import { acceptedAttachmentKind } from "@/hooks/useAttachedImages";
/** Extract supported attachment ``File``s from a paste / drop event.
*
* Deliberate behaviour:
* - Only items whose ``kind === "file"`` and ``type`` starts with
* ``image/`` are returned; ``<img>`` tags inside HTML fragments are
* ignored (defending against remote URL fetch + XSS surfaces).
* - Plain text pasted alongside images is *not* consumed by this helper,
* - Only items whose ``kind === "file"`` and match the Composer whitelist
* are returned; HTML fragments are ignored (defending against remote URL
* fetch + XSS surfaces).
* - Plain text pasted alongside attachments is *not* consumed by this helper,
* so the caller can still let the textarea receive it naturally.
*/
export function extractImageFilesFromPaste(
@@ -18,14 +20,13 @@ export function extractImageFilesFromPaste(
const files: File[] = [];
for (const item of Array.from(clipboard.items)) {
if (item.kind !== "file") continue;
if (!item.type.startsWith("image/")) continue;
const file = item.getAsFile();
if (file) files.push(file);
if (file && acceptedAttachmentKind(file)) files.push(file);
}
return files;
}
/** Extract dropped image files, mirroring ``extractImageFilesFromPaste``. */
/** Extract dropped attachment files, mirroring ``extractImageFilesFromPaste``. */
export function extractImageFilesFromDrop(
event: DragEvent | React.DragEvent,
): File[] {
@@ -34,7 +35,7 @@ export function extractImageFilesFromDrop(
if (!dt) return [];
const files: File[] = [];
for (const item of Array.from(dt.files)) {
if (item.type.startsWith("image/")) files.push(item);
if (acceptedAttachmentKind(item)) files.push(item);
}
return files;
}
@@ -67,8 +68,8 @@ export function useClipboardAndDrop(
(event: React.ClipboardEvent) => {
const files = extractImageFilesFromPaste(event);
if (files.length === 0) return;
// Consume only when an image is actually present; plain-text paste still
// reaches the textarea unmolested.
// Consume only when an attachment is actually present; plain-text paste
// still reaches the textarea unmolested.
event.preventDefault();
onImageFiles(files);
},