From 32643975b5ddb40322b8d5b6de79ccaa6e01bccc Mon Sep 17 00:00:00 2001 From: Soujit Das Date: Tue, 8 Oct 2024 11:27:28 +0530 Subject: [PATCH 1/9] Making the inital commit --- lib/plugin.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/plugin.js b/lib/plugin.js index c324fac5..c92cd457 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -41,6 +41,7 @@ cds.once("served", async function registerPluginHandlers() { srv.before("READ", [target, target.drafts], validateAttachment); srv.after("READ", [target, target.drafts], readAttachment); + srv.before("PUT", [target, target.drafts], computeAttachment); AttachmentsSrv.registerUpdateHandlers(srv, entity, target); @@ -56,8 +57,36 @@ cds.once("served", async function registerPluginHandlers() { } } + function computeAttachment(req) { + const contentLengthHeader = req.headers["content-length"]; + let fileSizeInBytes; + + if (contentLengthHeader) { + fileSizeInBytes = Number(contentLengthHeader); + + if (!isNaN(fileSizeInBytes)) { + console.log(`File size: ${fileSizeInBytes} bytes`); + const MAX_FILE_SIZE = 400 * 1024 * 1024; // 400 MB in bytes + + + if (fileSizeInBytes > MAX_FILE_SIZE) { + console.error( + "File size exceeds 400 MB limit. File will not be stored." + ); + // Reject the request or prevent file storage + + return req.reject(400, "File size exceeds the 400 MB limit."); + } + } else { + console.warn("Content-Length is not a valid number."); + return req.reject(400, "Invalid Content-Length header."); + } + } else { + return req.reject(400, "Missing Content-Length header."); + } + } + async function validateAttachment(req) { - /* removing case condition for mediaType annotation as in our case binary value and metadata is stored in different database */ req?.query?.SELECT?.columns?.forEach((element) => { @@ -144,5 +173,5 @@ const Ext2MimeTyes = { xml: "application/xml", zip: "application/zip", txt: "application/txt", - lst: "application/txt" + lst: "application/txt", }; From f342e8ff33ebe3fd0b7d0f6621736464695a6f66 Mon Sep 17 00:00:00 2001 From: Soujit Das Date: Fri, 18 Oct 2024 15:13:21 +0530 Subject: [PATCH 2/9] Added handler for validating size and unit test file. --- lib/plugin.js | 95 ++++++++++++----------- tests/unit/validateAttachmentSize.test.js | 35 +++++++++ 2 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 tests/unit/validateAttachmentSize.test.js diff --git a/lib/plugin.js b/lib/plugin.js index c92cd457..ab4c0d80 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -6,7 +6,7 @@ const attachmentIDRegex = /attachments\(.*ID=([^,]+),.*\)/; cds.on("loaded", function unfoldModel(csn) { if (!("Attachments" in csn.definitions)) return; - const csnCopy = structuredClone(csn) + const csnCopy = structuredClone(csn); cds.linked(csnCopy).forall("Composition", (comp) => { if (comp._target && comp._target["@_is_media_data"] && comp.parent && comp.is2many) { const parentDefinition = comp.parent.name @@ -31,25 +31,28 @@ cds.once("served", async function registerPluginHandlers() { for (let srv of cds.services) { if (srv instanceof cds.ApplicationService) { Object.values(srv.entities).forEach((entity) => { - for (let elementName in entity.elements) { if (elementName === "SiblingEntity") continue; // REVISIT: Why do we have this? - const element = entity.elements[elementName], target = element._target; + const element = entity.elements[elementName], + target = element._target; if (target?.["@_is_media_data"] && target?.drafts) { DEBUG?.("serving attachments for:", target.name); - + srv.before("READ", [target, target.drafts], validateAttachment); srv.after("READ", [target, target.drafts], readAttachment); - srv.before("PUT", [target, target.drafts], computeAttachment); + + srv.before("PUT", [target,target.drafts], (req) => validateAttachmentSize(req) ); + AttachmentsSrv.registerUpdateHandlers(srv, entity, target); - - srv.before('NEW', target.drafts, req => { + + srv.before("NEW", target.drafts, (req) => { req.data.url = cds.utils.uuid(); req.data.ID = cds.utils.uuid(); let ext = extname(req.data.filename).toLowerCase().slice(1); - req.data.mimeType = Ext2MimeTyes[ext] || "application/octet-stream"; + req.data.mimeType = + Ext2MimeTyes[ext] || "application/octet-stream"; }); } } @@ -57,63 +60,61 @@ cds.once("served", async function registerPluginHandlers() { } } - function computeAttachment(req) { - const contentLengthHeader = req.headers["content-length"]; - let fileSizeInBytes; - - if (contentLengthHeader) { - fileSizeInBytes = Number(contentLengthHeader); - - if (!isNaN(fileSizeInBytes)) { - console.log(`File size: ${fileSizeInBytes} bytes`); - const MAX_FILE_SIZE = 400 * 1024 * 1024; // 400 MB in bytes - - - if (fileSizeInBytes > MAX_FILE_SIZE) { - console.error( - "File size exceeds 400 MB limit. File will not be stored." - ); - // Reject the request or prevent file storage - - return req.reject(400, "File size exceeds the 400 MB limit."); - } - } else { - console.warn("Content-Length is not a valid number."); - return req.reject(400, "Invalid Content-Length header."); - } - } else { - return req.reject(400, "Missing Content-Length header."); - } - } - async function validateAttachment(req) { /* removing case condition for mediaType annotation as in our case binary value and metadata is stored in different database */ - + req?.query?.SELECT?.columns?.forEach((element) => { - if(element.as === 'content@odata.mediaContentType' && element.xpr){ + if (element.as === "content@odata.mediaContentType" && element.xpr) { delete element.xpr; - element.ref = ['mimeType']; + element.ref = ["mimeType"]; } }); - if(req?.req?.url?.endsWith("/content")) { + if (req?.req?.url?.endsWith("/content")) { const attachmentID = req.req.url.match(attachmentIDRegex)[1]; - const status = await AttachmentsSrv.getStatus(req.target, { ID : attachmentID }); - const scanEnabled = cds.env.requires?.attachments?.scan ?? true - if(scanEnabled && status !== 'Clean') { - req.reject(403, 'Unable to download the attachment as scan status is not clean.'); + const status = await AttachmentsSrv.getStatus(req.target, { + ID: attachmentID, + }); + const scanEnabled = cds.env.requires?.attachments?.scan ?? true; + if (scanEnabled && status !== "Clean") { + req.reject( + 403, + "Unable to download the attachment as scan status is not clean." + ); } } } async function readAttachment([attachment], req) { - if (!req?.req?.url?.endsWith("/content") || !attachment || attachment?.content) return; - let keys = { ID : req.req.url.match(attachmentIDRegex)[1]}; + if ( + !req?.req?.url?.endsWith("/content") || + !attachment || + attachment?.content + ) + return; + let keys = { ID: req.req.url.match(attachmentIDRegex)[1] }; let { target } = req; attachment.content = await AttachmentsSrv.get(target, keys, req); //Dependency -> sending req object for usage in SDM plugin } }); +function validateAttachmentSize(req) { + const contentLengthHeader = req.headers["content-length"]; + let fileSizeInBytes; + + if (contentLengthHeader) { + fileSizeInBytes = Number(contentLengthHeader); + const MAX_FILE_SIZE = 1 * 1024; + if (fileSizeInBytes > MAX_FILE_SIZE) { + return req.reject(403, "File Size limit exceeded beyond 400 MB."); + } + } else { + return req.reject(403, "Invalid Content Size"); + } +} + +module.exports = { validateAttachmentSize }; + const Ext2MimeTyes = { aac: "audio/aac", abw: "application/x-abiword", diff --git a/tests/unit/validateAttachmentSize.test.js b/tests/unit/validateAttachmentSize.test.js new file mode 100644 index 00000000..dedb0725 --- /dev/null +++ b/tests/unit/validateAttachmentSize.test.js @@ -0,0 +1,35 @@ +const { validateAttachmentSize } = require('../../lib/plugin'); + +describe('validateAttachmentSize', () => { + let req; // Define a mock request object + + beforeEach(() => { + req = { + headers: {}, + reject: jest.fn(), // Mocking the reject function + }; + }); + + it('should pass validation for a file size under 400 MB', () => { + req.headers['content-length'] = '51200765'; + + validateAttachmentSize(req); + + expect(req.reject).not.toHaveBeenCalled(); + }); + + it('should reject for a file size over 400 MB', () => { + req.headers['content-length'] = '20480000000'; + + validateAttachmentSize(req); + + expect(req.reject).toHaveBeenCalledWith(403, 'File Size limit exceeded beyond 400 MB.'); + }); + + it('should reject when content-length header is missing', () => { + validateAttachmentSize(req); + + expect(req.reject).toHaveBeenCalledWith(403, 'Invalid Content Size'); + }); +}); + From 7402a98bfb59dfa38001c8f806ff2918b04bc6e4 Mon Sep 17 00:00:00 2001 From: SoujitD-SAP Date: Tue, 22 Oct 2024 13:04:12 +0530 Subject: [PATCH 3/9] Update plugin.js rectified the test file size --- lib/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/plugin.js b/lib/plugin.js index ab4c0d80..4f1c81c3 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -104,7 +104,7 @@ function validateAttachmentSize(req) { if (contentLengthHeader) { fileSizeInBytes = Number(contentLengthHeader); - const MAX_FILE_SIZE = 1 * 1024; + const MAX_FILE_SIZE = 419430400; //400 MB in bytes if (fileSizeInBytes > MAX_FILE_SIZE) { return req.reject(403, "File Size limit exceeded beyond 400 MB."); } From c02e2ddf6b8556705da769897924a441b86fee32 Mon Sep 17 00:00:00 2001 From: Soujit Das Date: Wed, 23 Oct 2024 14:46:17 +0530 Subject: [PATCH 4/9] Updated CHANGELOG.md and plugin.js --- CHANGELOG.md | 6 ++++++ lib/plugin.js | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a520df..9a7236e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## Version 1.1.9 + +### Added + +- Size validation check for uploaded attachments. + ## Version 1.1.8 ### Changed diff --git a/lib/plugin.js b/lib/plugin.js index 4f1c81c3..d4230d03 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -42,7 +42,7 @@ cds.once("served", async function registerPluginHandlers() { srv.after("READ", [target, target.drafts], readAttachment); - srv.before("PUT", [target,target.drafts], (req) => validateAttachmentSize(req) ); + srv.before("PUT", target.drafts, (req) => validateAttachmentSize(req) ); AttachmentsSrv.registerUpdateHandlers(srv, entity, target); From c14f396ebffa2592d8166388340879169b1e5c9a Mon Sep 17 00:00:00 2001 From: Soujit Das Date: Thu, 24 Oct 2024 17:17:33 +0530 Subject: [PATCH 5/9] Update package.json, changelog, and plugin.js --- CHANGELOG.md | 6 ++---- lib/plugin.js | 11 ++--------- package.json | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a7236e0..1031adb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). -## Version 1.1.9 +## Version 1.1.8 ### Added -- Size validation check for uploaded attachments. - -## Version 1.1.8 +- Added limit for file size . ### Changed diff --git a/lib/plugin.js b/lib/plugin.js index d4230d03..5f0204e1 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -72,9 +72,7 @@ cds.once("served", async function registerPluginHandlers() { if (req?.req?.url?.endsWith("/content")) { const attachmentID = req.req.url.match(attachmentIDRegex)[1]; - const status = await AttachmentsSrv.getStatus(req.target, { - ID: attachmentID, - }); + const status = await AttachmentsSrv.getStatus(req.target, {ID: attachmentID,}); const scanEnabled = cds.env.requires?.attachments?.scan ?? true; if (scanEnabled && status !== "Clean") { req.reject( @@ -86,12 +84,7 @@ cds.once("served", async function registerPluginHandlers() { } async function readAttachment([attachment], req) { - if ( - !req?.req?.url?.endsWith("/content") || - !attachment || - attachment?.content - ) - return; + if (!req?.req?.url?.endsWith("/content") ||!attachment ||attachment?.content) return; let keys = { ID: req.req.url.match(attachmentIDRegex)[1] }; let { target } = req; attachment.content = await AttachmentsSrv.get(target, keys, req); //Dependency -> sending req object for usage in SDM plugin diff --git a/package.json b/package.json index 89320d0a..0ec73c38 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ ], "scripts": { "lint": "npx eslint .", - "test": "npx jest attachments.test.js" + "test": "npx jest" }, "dependencies": { "@aws-sdk/client-s3": "^3.400.0", From 44f91a3a3d2b61ad408e704feca3934e14246d4b Mon Sep 17 00:00:00 2001 From: SoujitD-SAP Date: Sat, 7 Dec 2024 22:49:34 +0530 Subject: [PATCH 6/9] Update CHANGELOG.md with UI5 version update --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a773d2..b1509a31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added -- Added limit for file size . +- - **File Size Validation**: Introduced a new file size validation feature to ensure uploaded attachments comply with defined size limits. +- This feature is compatible with SAPUI5 version `>= 1.131.0`. ### Changed From 6dfe6ce941001353d47105f31963faaad86d7305 Mon Sep 17 00:00:00 2001 From: SoujitD-SAP Date: Sat, 7 Dec 2024 22:50:19 +0530 Subject: [PATCH 7/9] Update CHANGELOG.md with UI5 version update --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1509a31..26602b2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added -- - **File Size Validation**: Introduced a new file size validation feature to ensure uploaded attachments comply with defined size limits. +- **File Size Validation**: Introduced a new file size validation feature to ensure uploaded attachments comply with defined size limits. - This feature is compatible with SAPUI5 version `>= 1.131.0`. ### Changed From 3655497afcbb13f83766abb3233d93d0c58d0fef Mon Sep 17 00:00:00 2001 From: SoujitD-SAP Date: Mon, 9 Dec 2024 15:12:35 +0530 Subject: [PATCH 8/9] Formatted plugin.js --- lib/plugin.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/plugin.js b/lib/plugin.js index 797013ed..d307a6e3 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -6,7 +6,7 @@ const attachmentIDRegex = /\/\w+\(.*ID=([0-9a-fA-F-]{36})/; cds.on("loaded", function unfoldModel(csn) { if (!("Attachments" in csn.definitions)) return; - const csnCopy = structuredClone(csn); + const csnCopy = structuredClone(csn) cds.linked(csnCopy).forall("Composition", (comp) => { if (comp._target && comp._target["@_is_media_data"] && comp.parent && comp.is2many) { const parentDefinition = comp.parent.name @@ -31,10 +31,10 @@ cds.once("served", async function registerPluginHandlers() { for (let srv of cds.services) { if (srv instanceof cds.ApplicationService) { Object.values(srv.entities).forEach((entity) => { + for (let elementName in entity.elements) { if (elementName === "SiblingEntity") continue; // REVISIT: Why do we have this? - const element = entity.elements[elementName], - target = element._target; + const element = entity.elements[elementName], target = element._target; if (target?.["@_is_media_data"] && target?.drafts) { DEBUG?.("serving attachments for:", target.name); @@ -44,15 +44,13 @@ cds.once("served", async function registerPluginHandlers() { srv.before("PUT", target.drafts, (req) => validateAttachmentSize(req) ); - AttachmentsSrv.registerUpdateHandlers(srv, entity, target); srv.before("NEW", target.drafts, (req) => { req.data.url = cds.utils.uuid(); req.data.ID = cds.utils.uuid(); let ext = extname(req.data.filename).toLowerCase().slice(1); - req.data.mimeType = - Ext2MimeTyes[ext] || "application/octet-stream"; + req.data.mimeType = Ext2MimeTyes[ext] || "application/octet-stream"; }); } } @@ -61,6 +59,7 @@ cds.once("served", async function registerPluginHandlers() { } async function validateAttachment(req) { + /* removing case condition for mediaType annotation as in our case binary value and metadata is stored in different database */ req?.query?.SELECT?.columns?.forEach((element) => { @@ -75,10 +74,7 @@ cds.once("served", async function registerPluginHandlers() { const status = await AttachmentsSrv.getStatus(req.target, {ID: attachmentID,}); const scanEnabled = cds.env.requires?.attachments?.scan ?? true; if (scanEnabled && status !== "Clean") { - req.reject( - 403, - "Unable to download the attachment as scan status is not clean." - ); + req.reject(403,"Unable to download the attachment as scan status is not clean."); } } } @@ -167,5 +163,5 @@ const Ext2MimeTyes = { xml: "application/xml", zip: "application/zip", txt: "application/txt", - lst: "application/txt", + lst: "application/txt" }; From e8a2c3301d7c72c5786f0e82faed1a3e1cc311a3 Mon Sep 17 00:00:00 2001 From: SoujitD-SAP Date: Tue, 10 Dec 2024 09:09:59 +0530 Subject: [PATCH 9/9] Formatted plugin.js --- lib/plugin.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/plugin.js b/lib/plugin.js index d307a6e3..c19d0494 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -37,7 +37,7 @@ cds.once("served", async function registerPluginHandlers() { const element = entity.elements[elementName], target = element._target; if (target?.["@_is_media_data"] && target?.drafts) { DEBUG?.("serving attachments for:", target.name); - + srv.before("READ", [target, target.drafts], validateAttachment); srv.after("READ", [target, target.drafts], readAttachment); @@ -45,8 +45,8 @@ cds.once("served", async function registerPluginHandlers() { srv.before("PUT", target.drafts, (req) => validateAttachmentSize(req) ); AttachmentsSrv.registerUpdateHandlers(srv, entity, target); - - srv.before("NEW", target.drafts, (req) => { + + srv.before('NEW', target.drafts, req => { req.data.url = cds.utils.uuid(); req.data.ID = cds.utils.uuid(); let ext = extname(req.data.filename).toLowerCase().slice(1); @@ -61,27 +61,27 @@ cds.once("served", async function registerPluginHandlers() { async function validateAttachment(req) { /* removing case condition for mediaType annotation as in our case binary value and metadata is stored in different database */ - + req?.query?.SELECT?.columns?.forEach((element) => { - if (element.as === "content@odata.mediaContentType" && element.xpr) { + if(element.as === 'content@odata.mediaContentType' && element.xpr){ delete element.xpr; - element.ref = ["mimeType"]; + element.ref = ['mimeType']; } }); - if (req?.req?.url?.endsWith("/content")) { + if(req?.req?.url?.endsWith("/content")) { const attachmentID = req.req.url.match(attachmentIDRegex)[1]; - const status = await AttachmentsSrv.getStatus(req.target, {ID: attachmentID,}); - const scanEnabled = cds.env.requires?.attachments?.scan ?? true; - if (scanEnabled && status !== "Clean") { - req.reject(403,"Unable to download the attachment as scan status is not clean."); + const status = await AttachmentsSrv.getStatus(req.target, { ID : attachmentID }); + const scanEnabled = cds.env.requires?.attachments?.scan ?? true + if(scanEnabled && status !== 'Clean') { + req.reject(403, 'Unable to download the attachment as scan status is not clean.'); } } } async function readAttachment([attachment], req) { - if (!req?.req?.url?.endsWith("/content") ||!attachment ||attachment?.content) return; - let keys = { ID: req.req.url.match(attachmentIDRegex)[1] }; + if (!req?.req?.url?.endsWith("/content") || !attachment || attachment?.content) return; + let keys = { ID : req.req.url.match(attachmentIDRegex)[1]}; let { target } = req; attachment.content = await AttachmentsSrv.get(target, keys, req); //Dependency -> sending req object for usage in SDM plugin }