Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"json-stringify-safe": "^5.0.1",
"lodash": "^4.18.1",
"luxon": "^3.7.2",
"media-typer": "^1.1.1",
"reflect-metadata": "^0.2.2",
"ts-simple-nameof": "^1.3.3"
},
Expand All @@ -105,6 +106,7 @@
"@types/json-stringify-safe": "5.0.3",
"@types/lodash": "4.17.25",
"@types/luxon": "3.7.5",
"@types/media-typer": "0.3.4",
"jwt-decode": "4.0.0",
"openid-client": "6.8.8",
"ts-json-schema-generator": "2.9.0",
Expand Down
47 changes: 47 additions & 0 deletions packages/runtime/src/useCases/transport/files/UploadOwnFile.ts
Comment thread
Milena-Czierlinski marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CoreBuffer } from "@nmshd/crypto";
import { FileDTO } from "@nmshd/runtime-types";
import { AccountController, FileController } from "@nmshd/transport";
import { Inject } from "@nmshd/typescript-ioc";
import { parse as parseMediaType } from "media-typer";
import { nameof } from "ts-simple-nameof";
import { ISO8601DateTimeString, RuntimeErrors, SchemaRepository, SchemaValidator, UseCase, ValidationFailure, ValidationResult } from "../../common";
import { FileMapper } from "./FileMapper";
Expand Down Expand Up @@ -40,6 +41,26 @@ class Validator extends SchemaValidator<UploadOwnFileValidatableRequest> {
const validationResult = super.validate(input);
if (!validationResult.isValid()) return validationResult;

const filenameValidationError = this.validateFilename(input.filename);
if (filenameValidationError) {
validationResult.addFailure(
new ValidationFailure(
RuntimeErrors.general.invalidPropertyValue(filenameValidationError),
nameof<UploadOwnFileValidatableRequest>((r) => r.filename)
)
);
}

const mimetypeValidationError = this.validateMimetype(input.mimetype);
if (mimetypeValidationError) {
validationResult.addFailure(
new ValidationFailure(
RuntimeErrors.general.invalidPropertyValue(mimetypeValidationError),
nameof<UploadOwnFileValidatableRequest>((r) => r.mimetype)
)
);
}

if (input.content.byteLength > this._maxFileSize) {
validationResult.addFailure(
new ValidationFailure(
Expand All @@ -60,6 +81,32 @@ class Validator extends SchemaValidator<UploadOwnFileValidatableRequest> {

return validationResult;
}

private validateFilename(filename: string): string | undefined {
const propertyName = nameof<UploadOwnFileValidatableRequest>((r) => r.filename);

if (filename.trim().length === 0) return `'${propertyName}' must not be empty or consist only of whitespace`;
if (filename === "." || filename === "..") return `'${propertyName}' must not be '.' or '..'`;
if (/[\\/]/.test(filename)) return `'${propertyName}' must not contain path separators`;
if (/\p{Cc}/u.test(filename)) return `'${propertyName}' must not contain Unicode control characters`;
if (new TextEncoder().encode(filename).byteLength > 255) return `'${propertyName}' must not exceed 255 UTF-8 bytes`;

return undefined;
}

private validateMimetype(mimetype: string): string | undefined {
const propertyName = nameof<UploadOwnFileValidatableRequest>((r) => r.mimetype);

if (mimetype.trim().length === 0) return `'${propertyName}' must not be empty or consist only of whitespace`;
if (mimetype.trim() !== mimetype) return `'${propertyName}' must not contain leading or trailing whitespace`;

try {
parseMediaType(mimetype);
return undefined;
} catch {
return `'${propertyName}' must be a concrete media type in the form 'type/subtype' without parameters or wildcards`;
}
}
}

export class UploadOwnFileUseCase extends UseCase<UploadOwnFileRequest, FileDTO> {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/test/lib/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export async function makeUploadRequest(values: object = {}): Promise<UploadOwnF
title: "aTitle",
filename: "aFileName",
content: await fs.promises.readFile(fileURLToPath(new URL("../__assets__/test.txt", import.meta.url))),
mimetype: "aMimetype",
mimetype: "text/plain",
description: "aDescription",
expiresAt: DateTime.utc().plus({ minutes: 5 }).toString(),
...values
Expand Down
39 changes: 39 additions & 0 deletions packages/runtime/test/transport/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,45 @@ describe("File upload", () => {
expect(response).toBeSuccessful();
});

test.each(["test.txt", "a file.txt", "ÄÖÜ.txt"])("can upload a file named '%s'", async (filename) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ filename }));

expect(response).toBeSuccessful();
expect(response.value.filename).toBe(filename);
});

test.each([
["an empty filename", ""],
["a filename consisting only of whitespace", " \t "],
["'.' as filename", "."],
["'..' as filename", ".."],
["a forward slash in the filename", "directory/file.txt"],
["a backslash in the filename", "directory\\file.txt"],
["a NUL character in the filename", "file\0.txt"],
["another control character in the filename", "file\u0085.txt"],
["a filename longer than 255 UTF-8 bytes", "ä".repeat(128)]
])("cannot upload a file with %s", async (_description, filename) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ filename }));

expect(response).toBeAnError(/filename/, "error.runtime.validation.invalidPropertyValue");
});

test.each(["text/plain", "application/pdf", "image/svg+xml", "application/vnd.api+json", "IMAGE/PNG"])("can upload a file with MIME type '%s'", async (mimetype) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ mimetype }));

expect(response).toBeSuccessful();
expect(response.value.mimetype).toBe(mimetype);
});

test.each(["", " \t ", "text", "text/", "/plain", "text/pla in", "text/plain; charset=utf-8", "text/*", "*/*", "text/plain\r\n"])(
"cannot upload a file with invalid MIME type '%s'",
async (mimetype) => {
const response = await transportServices1.files.uploadOwnFile(await makeUploadRequest({ mimetype }));

expect(response).toBeAnError(/mimetype/, "error.runtime.validation.invalidPropertyValue");
}
);

test("uploaded files can be accessed under /Files", async () => {
const uploadResponse = await transportServices1.files.uploadOwnFile(await makeUploadRequest());
expect(uploadResponse).toBeSuccessful();
Expand Down
Loading