import { uploadSingleFile } from "../../../actions/azurestorage"; import nextConnect from "next-connect"; import middleware from "../middleware/middleware"; import { consoleLogger } from "../../../actions/core/logger"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; import { fileTypeFromBuffer } from "file-type"; import fs from "fs"; import path from "path"; const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-:()—']+$/; const MAX_FILES_PER_UPLOAD_BATCH = Math.max( 1, Number.parseInt(process.env.UPLOAD_BATCH_COUNT || "5", 10) || 5 ); function validateFilenameServer(originalFilename) { const name = path.basename(originalFilename || ""); if (!name) return { ok: false, reason: "Empty filename" }; if (name.includes("#")) return { ok: false, reason: "Filename contains #" }; if (/[<>"/\\|?*]/.test(name)) return { ok: false, reason: "Filename contains reserved characters" }; if (!FILENAME_ALLOWED.test(name)) return { ok: false, reason: "Filename contains invalid characters" }; if (name === "." || name === "..") return { ok: false, reason: "Invalid filename" }; return { ok: true, name }; } const ApiProxy = nextConnect(); ApiProxy.use(middleware); ApiProxy.post(async (req, res) => { const checkHash = req.query.hash; if (typeof checkHash === "undefined" || checkHash.length === 0) { return respondError(res, { status: 400, code: "HASH_REQUIRED", message: "hash is required" }); } const hashCandidatePaths = [ "/api/file/uploadsinglefile", "/api/file/uploadsinglefile?" ]; const isHashValid = hashCandidatePaths.some( (candidatePath) => hashAPIPath(candidatePath) == "?hash=" + checkHash ); if (!isHashValid) { return respondError(res, { status: 400, code: "INVALID_HASH", message: "Invalid hash" }); } const containerID = req.body?.containerID?.[0]; const casefolderID = req.body?.casefolderID?.[0]; if ( typeof containerID === "undefined" || containerID.length === 0 || typeof casefolderID === "undefined" || casefolderID.length === 0 ) { return respondError(res, { status: 400, code: "MISSING_REQUIRED_BODY", message: "containerID and casefolderID are required" }); } const uploadedFiles = req.files || {}; // Add other mimetypes here const allowedMimeTypes = [ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "image/tiff", "image/jpeg", "application/zip", "image/png", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ]; const allowedFilesFormData = {}; const invalidFiles = []; // To store the names of invalid files for (const [fileName, fileDetails] of Object.entries(uploadedFiles)) { const file = fileDetails[0]; const filePath = file.path; // validate ORIGINAL filename from upload const fnCheck = validateFilenameServer(file.originalFilename); if (!fnCheck.ok) { invalidFiles.push(`${file.originalFilename} - Invalid filename`); continue; // do not process further } // Check the MIME type from file headers const fileMimeType = file.headers["content-type"]; if (allowedMimeTypes.includes(fileMimeType)) { // Check file signature (magic number) using file-type const buffer = fs.readFileSync(filePath); try { const type = await fileTypeFromBuffer(buffer); // Correct usage of fileTypeFromBuffer if (type && allowedMimeTypes.includes(type.mime)) { // If the MIME type from the file signature matches the allowed list allowedFilesFormData[fileName] = fileDetails.map( (file) => ({ fieldName: file.fieldName, originalFilename: file.originalFilename, path: file.path, size: file.size, headers: file.headers, contentType: fileMimeType }) ); } else { invalidFiles.push(fileName); // Track invalid file } } catch (error) { consoleLogger(error); } } else { invalidFiles.push(fileName); // Track invalid file } } try { const allowedFileEntries = Object.entries(allowedFilesFormData); const data = []; for ( let index = 0; index < allowedFileEntries.length; index += MAX_FILES_PER_UPLOAD_BATCH ) { const fileChunk = Object.fromEntries( allowedFileEntries.slice( index, index + MAX_FILES_PER_UPLOAD_BATCH ) ); const uploadChunkResult = await uploadSingleFile( fileChunk, containerID, casefolderID ); if (Array.isArray(uploadChunkResult)) { data.push(...uploadChunkResult); } } return respondSuccess(res, { data, invalidFiles }); } catch (error) { consoleLogger(error); return respondError(res, { status: 500, code: "UPLOAD_SINGLE_FILE_FAILED", message: "Failed to upload files" }); } }); export const config = { api: { bodyParser: false } }; export default ApiProxy;