mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 10:14:45 +00:00
fix(media): avoid staged image extensions for containers
This commit is contained in:
@@ -17,7 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. (#82443) Thanks @joshavant.
|
||||
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media.
|
||||
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media or keep image file extensions when staged.
|
||||
- Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs.
|
||||
- Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments.
|
||||
- Plugins/web search: downgrade stale optional provider installs to warnings so Gateway and doctor repair paths keep running after startup provider selection. Refs #82313. Thanks @crackmac.
|
||||
|
||||
@@ -670,6 +670,21 @@ describe("media store", () => {
|
||||
expectedContentType: "application/octet-stream",
|
||||
expectedExtension: ".custom",
|
||||
},
|
||||
{
|
||||
name: "does not preserve image header extensions for generic container buffers",
|
||||
bufferFactory: async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file("hello.txt", "hi");
|
||||
return await zip.generateAsync({ type: "nodebuffer" });
|
||||
},
|
||||
contentType: "image/png",
|
||||
originalFilename: "fake.png",
|
||||
expectedContentType: "application/zip",
|
||||
expectedExtension: ".zip",
|
||||
assertSaved: async (saved: Awaited<ReturnType<typeof store.saveMediaBuffer>>) => {
|
||||
expect(path.basename(saved.path)).toMatch(/^fake---[a-f0-9-]{36}\.zip$/);
|
||||
},
|
||||
},
|
||||
] as const)("$name", async (testCase) => {
|
||||
const buffer =
|
||||
"bufferFactory" in testCase && testCase.bufferFactory
|
||||
|
||||
@@ -329,6 +329,34 @@ function extensionForAuthoritativeHeaderMime(contentType?: string): string | und
|
||||
return extensionForMime(mime);
|
||||
}
|
||||
|
||||
function isGenericContainerMime(mime?: string): boolean {
|
||||
return mime === "application/zip" || mime === "application/octet-stream";
|
||||
}
|
||||
|
||||
function isImageHeaderMime(contentType?: string): boolean {
|
||||
return normalizeOptionalString(contentType?.split(";")[0])?.startsWith("image/") === true;
|
||||
}
|
||||
|
||||
function resolveSavedMediaExtension(params: {
|
||||
detectedMime?: string;
|
||||
headerExt?: string;
|
||||
contentType?: string;
|
||||
originalFilename?: string;
|
||||
}): string {
|
||||
const trustedHeaderExt =
|
||||
params.headerExt &&
|
||||
isGenericContainerMime(params.detectedMime) &&
|
||||
isImageHeaderMime(params.contentType)
|
||||
? undefined
|
||||
: params.headerExt;
|
||||
return (
|
||||
trustedHeaderExt ??
|
||||
extensionForMime(params.detectedMime) ??
|
||||
safeOriginalFilenameExtension(params.originalFilename) ??
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function buildSavedMediaResult(params: {
|
||||
dir: string;
|
||||
id: string;
|
||||
@@ -532,8 +560,12 @@ export async function saveMediaBuffer(
|
||||
headerMime: contentType,
|
||||
filePath: originalFilename ?? detectionFilePathHint,
|
||||
});
|
||||
const ext =
|
||||
headerExt ?? extensionForMime(mime) ?? safeOriginalFilenameExtension(originalFilename) ?? "";
|
||||
const ext = resolveSavedMediaExtension({
|
||||
detectedMime: mime,
|
||||
headerExt,
|
||||
contentType,
|
||||
originalFilename,
|
||||
});
|
||||
const id = buildSavedMediaId({ baseId: uuid, ext, originalFilename });
|
||||
await writeSavedMediaBuffer({ subdir, id, buffer });
|
||||
return buildSavedMediaResult({ dir, id, size: buffer.byteLength, contentType: mime });
|
||||
@@ -567,11 +599,12 @@ export async function saveMediaStream(
|
||||
headerMime: contentType,
|
||||
filePath: originalFilename ?? detectionFilePathHint,
|
||||
});
|
||||
const ext =
|
||||
headerExt ??
|
||||
extensionForMime(mime) ??
|
||||
safeOriginalFilenameExtension(originalFilename) ??
|
||||
"";
|
||||
const ext = resolveSavedMediaExtension({
|
||||
detectedMime: mime,
|
||||
headerExt,
|
||||
contentType,
|
||||
originalFilename,
|
||||
});
|
||||
const id = buildSavedMediaId({ baseId, ext, originalFilename });
|
||||
return { id, size, contentType: mime };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user