diff --git a/apps/api/plane/settings/common.py b/apps/api/plane/settings/common.py index 25a212e7639..3169ca36926 100644 --- a/apps/api/plane/settings/common.py +++ b/apps/api/plane/settings/common.py @@ -533,6 +533,7 @@ def _retention_days(env_var, default): "text/css", "text/javascript", "application/json", + "application/yaml", "text/xml", "text/csv", "application/xml", diff --git a/packages/services/package.json b/packages/services/package.json index c76d4a6c081..c647ef06f0e 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -14,6 +14,7 @@ "scripts": { "build": "tsdown", "dev": "tsdown --watch --no-clean", + "test": "vitest run", "check:lint": "oxlint --max-warnings=6 .", "check:types": "tsc --noEmit", "check:format": "oxfmt --check .", @@ -30,6 +31,7 @@ "devDependencies": { "@plane/typescript-config": "workspace:*", "tsdown": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "catalog:" } } diff --git a/packages/services/src/file/helper.test.ts b/packages/services/src/file/helper.test.ts new file mode 100644 index 00000000000..a5666f7f886 --- /dev/null +++ b/packages/services/src/file/helper.test.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { describe, expect, it, vi } from "vitest"; + +import { getFileMetaDataForUpload } from "./helper"; + +const metadata = (name: string, type = "", contents: BlobPart[] = ["key: value"]) => + getFileMetaDataForUpload(new File(contents, name, { type })); + +describe("getFileMetaDataForUpload", () => { + it.each([ + ["program.txt", "text/plain"], + ["program.md", "text/markdown"], + ["program.markdown", "text/markdown"], + ["program.csv", "text/csv"], + ["program.yaml", "application/yaml"], + ["program.yml", "application/yaml"], + ])("detects %s without browser MIME", async (name, expected) => { + expect((await metadata(name)).type).toBe(expected); + }); + + it("falls back to browser MIME", async () => { + expect((await metadata("program.log", "text/plain")).type).toBe("text/plain"); + }); + + it("prefers signature detection", async () => { + const pngHeader = new Uint8Array([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, + ]); + + expect((await metadata("image.yaml", "application/yaml", [pngHeader])).type).toBe("image/png"); + }); + + it("does not apply fallback to an unsafe filename", async () => { + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => undefined); + + try { + expect((await metadata("payload.exe.yaml")).type).toBe(""); + } finally { + warnSpy.mockRestore(); + } + }); +}); diff --git a/packages/services/src/file/helper.ts b/packages/services/src/file/helper.ts index b8e96283986..f775ac8ab74 100644 --- a/packages/services/src/file/helper.ts +++ b/packages/services/src/file/helper.ts @@ -10,6 +10,15 @@ import { fileTypeFromBuffer } from "file-type"; import type { TFileMetaDataLite, TFileSignedURLResponse } from "@plane/types"; import { DANGEROUS_EXTENSIONS } from "@plane/constants"; +const TEXT_MIME_TYPES_BY_EXTENSION: Record = { + txt: "text/plain", + md: "text/markdown", + markdown: "text/markdown", + csv: "text/csv", + yaml: "application/yaml", + yml: "application/yaml", +}; + /** * @description Filename validation - checks for double extensions and dangerous patterns * @param {string} filename @@ -92,6 +101,7 @@ const validateAndDetectFileType = async (file: File): Promise => { const filenameError = validateFilename(file.name); if (filenameError) { console.warn(`File validation warning: ${filenameError}`); + return ""; } try { @@ -103,8 +113,8 @@ const validateAndDetectFileType = async (file: File): Promise => { console.warn("Error detecting file type from signature:", _error); } - // fallback for unknown files - return ""; + const extension = file.name.split(".").pop()?.toLowerCase() ?? ""; + return TEXT_MIME_TYPES_BY_EXTENSION[extension] || file.type || ""; }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbf11e23d18..fe42621e890 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1665,6 +1665,9 @@ importers: typescript: specifier: 5.8.3 version: 5.8.3 + vitest: + specifier: 'catalog:' + version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.12.0)(@vitest/coverage-v8@4.1.8)(vite@8.0.16(@types/node@22.12.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.3)) packages/shared-state: dependencies: