diff --git a/server/commands/documentImporter.js b/server/commands/documentImporter.js index cb8ff43d..da7d1c65 100644 --- a/server/commands/documentImporter.js +++ b/server/commands/documentImporter.js @@ -11,6 +11,7 @@ import parseTitle from "../../shared/utils/parseTitle"; import { FileImportError, InvalidRequestError } from "../errors"; import { User } from "../models"; import dataURItoBuffer from "../utils/dataURItoBuffer"; +import { deserializeFilename } from "../utils/fs"; import parseImages from "../utils/parseImages"; import attachmentCreator from "./attachmentCreator"; @@ -152,7 +153,7 @@ export default async function documentImporter({ if (!fileInfo) { throw new InvalidRequestError(`File type ${file.type} not supported`); } - let title = file.name.replace(/\.[^/.]+$/, ""); + let title = deserializeFilename(file.name.replace(/\.[^/.]+$/, "")); let text = await fileInfo.getMarkdown(file); // If the first line of the imported text looks like a markdown heading diff --git a/server/commands/documentImporter.test.js b/server/commands/documentImporter.test.js index efd91821..2324c30d 100644 --- a/server/commands/documentImporter.test.js +++ b/server/commands/documentImporter.test.js @@ -94,6 +94,25 @@ describe("documentImporter", () => { expect(response.title).toEqual("Heading 1"); }); + it("should handle encoded slashes", async () => { + const user = await buildUser(); + const name = "this %2F and %2F this.md"; + const file = new File({ + name, + type: "text/plain", + path: path.resolve(__dirname, "..", "test", "fixtures", "empty.md"), + }); + + const response = await documentImporter({ + user, + file, + ip, + }); + + expect(response.text).toContain(""); + expect(response.title).toEqual("this / and / this"); + }); + it("should fallback to extension if mimetype unknown", async () => { const user = await buildUser(); const name = "markdown.md"; diff --git a/server/test/fixtures/empty.md b/server/test/fixtures/empty.md new file mode 100644 index 00000000..e69de29b diff --git a/server/utils/fs.js b/server/utils/fs.js index 32012ceb..86d5df8e 100644 --- a/server/utils/fs.js +++ b/server/utils/fs.js @@ -2,6 +2,14 @@ import path from "path"; import fs from "fs-extra"; +export function serializeFilename(text: string): string { + return text.replace(/\//g, "%2F").replace(/\\/g, "%5C"); +} + +export function deserializeFilename(text: string): string { + return text.replace(/%2F/g, "/").replace(/%5C/g, "\\"); +} + export function requireDirectory(dirName: string): [T, string][] { return fs .readdirSync(dirName) diff --git a/server/utils/fs.test.js b/server/utils/fs.test.js new file mode 100644 index 00000000..71dadb23 --- /dev/null +++ b/server/utils/fs.test.js @@ -0,0 +1,34 @@ +// @flow +import { serializeFilename, deserializeFilename } from "./fs"; + +describe("serializeFilename", () => { + it("should serialize forward slashes", () => { + expect(serializeFilename(`/`)).toBe("%2F"); + expect(serializeFilename(`this / and / this`)).toBe( + "this %2F and %2F this" + ); + }); + + it("should serialize back slashes", () => { + expect(serializeFilename(`\\`)).toBe("%5C"); + expect(serializeFilename(`this \\ and \\ this`)).toBe( + "this %5C and %5C this" + ); + }); +}); + +describe("deserializeFilename", () => { + it("should deserialize forward slashes", () => { + expect(deserializeFilename("%2F")).toBe("/"); + expect(deserializeFilename("this %2F and %2F this")).toBe( + `this / and / this` + ); + }); + + it("should deserialize back slashes", () => { + expect(deserializeFilename("%5C")).toBe(`\\`); + expect(deserializeFilename("this %5C and %5C this")).toBe( + `this \\ and \\ this` + ); + }); +}); diff --git a/server/utils/zip.js b/server/utils/zip.js index 50b67878..da8f6a2f 100644 --- a/server/utils/zip.js +++ b/server/utils/zip.js @@ -4,6 +4,7 @@ import * as Sentry from "@sentry/node"; import JSZip from "jszip"; import tmp from "tmp"; import { Attachment, Collection, Document } from "../models"; +import { serializeFilename } from "./fs"; import { getFileByKey } from "./s3"; async function addToArchive(zip, documents) { @@ -23,7 +24,9 @@ async function addToArchive(zip, documents) { text = text.replace(attachment.redirectUrl, encodeURI(attachment.key)); } - zip.file(`${document.title || "Untitled"}.md`, text, { + const title = serializeFilename(document.title) || "Untitled"; + + zip.file(`${title}.md`, text, { date: document.updatedAt, comment: JSON.stringify({ pinned: document.pinned, @@ -33,7 +36,7 @@ async function addToArchive(zip, documents) { }); if (doc.children && doc.children.length) { - const folder = zip.folder(document.title); + const folder = zip.folder(title); await addToArchive(folder, doc.children); } }