Files
OpenSign/apps/OpenSignServer/Utils.js
T
2026-05-22 21:03:28 +05:30

821 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dotenv from 'dotenv';
import { format, toZonedTime } from 'date-fns-tz';
import getPresignedUrl, { getSignedLocalUrl } from './cloud/parsefunction/getSignedUrl.js';
import crypto from 'node:crypto';
import {
PDFDocument,
PDFName,
rgb,
degrees,
// StandardFonts,
PDFArray,
PDFDict,
} from 'pdf-lib';
import { parseUploadFile } from './utils/fileUtils.js';
dotenv.config({ quiet: true });
export const cloudServerUrl = 'http://localhost:8080/app';
export const serverAppId = process.env.APP_ID || 'opensign';
export const appName = 'OpenSign™';
export const prefillDraftDocWidget = ['date', 'textbox', 'checkbox', 'radio button', 'image'];
export const prefillDraftTemWidget = [
'date',
'textbox',
'checkbox',
'radio button',
'image',
'dropdown',
];
export const MAX_NAME_LENGTH = 250;
export const MAX_NOTE_LENGTH = 200;
export const MAX_DESCRIPTION_LENGTH = 500;
export const color = [
'#93a3db',
'#e6c3db',
'#c0e3bc',
'#bce3db',
'#b8ccdb',
'#ceb8db',
'#ffccff',
'#99ffcc',
'#cc99ff',
'#ffcc99',
'#66ccff',
'#ffffcc',
];
export const prefillBlockColor = 'transparent';
export function replaceMailVaribles(subject, body, variables) {
let replacedSubject = subject;
let replacedBody = body;
for (const variable in variables) {
const regex = new RegExp(`{{${variable}}}`, 'g');
if (subject) {
replacedSubject = replacedSubject.replace(regex, variables[variable]);
}
if (body) {
replacedBody = replacedBody.replace(regex, variables[variable]);
}
}
const result = { subject: replacedSubject, body: replacedBody };
return result;
}
export const saveFileUsage = async (size, fileUrl, userId) => {
//checking server url and save file's size
try {
if (userId) {
const userPtr = { __type: 'Pointer', className: '_User', objectId: userId };
const tenantQuery = new Parse.Query('partners_Tenant');
tenantQuery.equalTo('UserId', userPtr);
const tenant = await tenantQuery.first({ useMasterKey: true });
if (tenant) {
const tenantPtr = { __type: 'Pointer', className: 'partners_Tenant', objectId: tenant.id };
try {
const tenantCredits = new Parse.Query('partners_TenantCredits');
tenantCredits.equalTo('PartnersTenant', tenantPtr);
const res = await tenantCredits.first({ useMasterKey: true });
if (res) {
const response = JSON.parse(JSON.stringify(res));
const usedStorage = response?.usedStorage ? response.usedStorage + size : size;
const updateCredit = new Parse.Object('partners_TenantCredits');
updateCredit.id = res.id;
updateCredit.set('usedStorage', usedStorage);
await updateCredit.save(null, { useMasterKey: true });
} else {
const newCredit = new Parse.Object('partners_TenantCredits');
newCredit.set('usedStorage', size);
newCredit.set('PartnersTenant', tenantPtr);
await newCredit.save(null, { useMasterKey: true });
}
} catch (err) {
console.log('err in save usage', err);
}
saveDataFile(size, fileUrl, tenantPtr, userPtr);
}
}
} catch (err) {
console.log('err in fetch tenant Id', err);
}
};
//function for save fileUrl and file size in particular client db class partners_DataFiles
const saveDataFile = async (size, fileUrl, tenantPtr, UserId) => {
try {
const newDataFiles = new Parse.Object('partners_DataFiles');
newDataFiles.set('FileUrl', fileUrl);
newDataFiles.set('FileSize', size);
newDataFiles.set('TenantPtr', tenantPtr);
newDataFiles.set('UserId', UserId);
await newDataFiles.save(null, { useMasterKey: true });
} catch (err) {
console.log('error in save usage ', err);
}
};
export const updateMailCount = async extUserId => {
// Update count in contracts_Users class
const query = new Parse.Query('contracts_Users');
query.equalTo('objectId', extUserId);
try {
const contractUser = await query.first({ useMasterKey: true });
if (contractUser) {
const _extRes = JSON.parse(JSON.stringify(contractUser));
contractUser.increment('EmailCount', 1);
await contractUser.save(null, { useMasterKey: true });
}
} catch (error) {
console.log('Error updating EmailCount in contracts_Users: ' + error.message);
}
};
export function sanitizeFileName(fileName) {
// Remove spaces and invalid characters
const file = fileName.replace(/[^a-zA-Z0-9._-]/g, '');
const removedot = file.replace(/\.(?=.*\.)/g, '');
return removedot.replace(/[^a-zA-Z0-9._-]/g, '');
}
export const useLocal = process.env.USE_LOCAL ? process.env.USE_LOCAL.toLowerCase() : 'false';
export const smtpsecure = process.env.SMTP_PORT && process.env.SMTP_PORT !== '465' ? false : true;
export const smtpenable =
process.env.SMTP_ENABLE && process.env.SMTP_ENABLE.toLowerCase() === 'true' ? true : false;
export const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// `generateId` is used to unique Id for fileAdapter
export function generateId(length) {
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* FlattenPdf renders field values as static content and removes the interactive
* form layer. Signatures are stripped entirely. Non-widget annotations (links,
* comments, stamps) are preserved.
* @param {string | Uint8Array | ArrayBuffer} pdfFile - pdf file.
* @returns {Promise<Uint8Array>} flatPdf - pdf file in Uint8Array
*/
export const flattenPdf = async pdfFile => {
const pdfDoc = await PDFDocument.load(pdfFile, { ignoreEncryption: true });
try {
const acroFormEntry = pdfDoc.catalog.get(PDFName.of('AcroForm'));
const acroForm = pdfDoc.context.lookupMaybe
? pdfDoc.context.lookupMaybe(acroFormEntry)
: pdfDoc.context.lookup(acroFormEntry);
if (acroForm && typeof acroForm.set === 'function') {
// Avoid pdf-lib form APIs here; some malformed PDFs crash while
// iterating/removing fields. Clearing /Fields directly is safer.
acroForm.set(PDFName.of('Fields'), pdfDoc.context.obj([]));
acroForm.delete(PDFName.of('XFA'));
acroForm.delete(PDFName.of('SigFlags'));
}
} catch {
// If AcroForm is malformed, continue with page annotation cleanup.
}
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of('Annots'));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter(annotRef => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of('Subtype'));
return subtype?.toString() !== '/Widget';
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of('Annots'));
} else {
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
}
} catch {
// best effort cleanup
}
}
try {
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
} catch {
// best effort cleanup
}
return await pdfDoc.save({ useObjectStreams: false });
};
/* ---- flattenPdf private helpers ---- */
function _safeGetWidgetsServer(field) {
try {
return field.acroField?.getWidgets?.() || [];
} catch {
return [];
}
}
function _getWidgetRectServer(widget, pdfDoc) {
try {
const r = widget.getRectangle?.();
if (r && isFinite(r.x) && isFinite(r.y) && isFinite(r.width) && isFinite(r.height)) {
return r;
}
} catch {
/* fall through to manual extraction */
}
try {
const rectArr = widget.dict?.lookup?.(PDFName.of('Rect'));
if (!rectArr || typeof rectArr.size !== 'function' || rectArr.size() !== 4) return null;
const x1 = _numberFromPdfObjectServer(rectArr.get(0));
const y1 = _numberFromPdfObjectServer(rectArr.get(1));
const x2 = _numberFromPdfObjectServer(rectArr.get(2));
const y2 = _numberFromPdfObjectServer(rectArr.get(3));
if ([x1, y1, x2, y2].some(v => !isFinite(v))) return null;
return {
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
};
} catch {
return null;
}
}
function _numberFromPdfObjectServer(obj) {
if (!obj) return NaN;
if (typeof obj.asNumber === 'function') return obj.asNumber();
if (typeof obj.numberValue === 'function') return obj.numberValue();
return Number(obj?.value ?? obj);
}
function _getWidgetPageServer(pdfDoc, pages, widget) {
try {
const pRef = widget.P?.();
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
try {
const pRef = widget.dict?.get?.(PDFName.of('P'));
if (pRef) {
for (const page of pages) {
if (page.ref === pRef) return page;
}
}
} catch {
/* fall through */
}
// Fallback: search page annots
try {
for (const page of pages) {
const annots = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray);
if (!annots) continue;
for (let i = 0; i < annots.size(); i++) {
const ref = annots.get(i);
if (ref === widget.ref) return page;
}
}
} catch {
/* fall through */
}
return null;
}
function _drawWidgetBoxServer(page, rect) {
try {
page.drawRectangle({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
borderWidth: 0.6,
borderColor: rgb(0.65, 0.65, 0.65),
});
} catch {
/* best effort */
}
}
function _drawTextFieldServer(page, field, rect, font) {
let text = '';
try {
text = field.getText?.() ?? '';
} catch {
text = '';
}
text = String(text ?? '');
if (!text) return;
let multiline = false;
try {
multiline = field.isMultiline?.() ?? false;
} catch {
/* ignore */
}
let comb = false;
try {
comb = field.isCombed?.() ?? false;
} catch {
/* ignore */
}
if (comb) {
_drawCombTextServer(page, text, rect, font);
return;
}
if (multiline || text.includes('\n')) {
_drawMultilineTextServer(page, text, rect, font);
return;
}
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
const baselineY = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(text, {
x: rect.x + 2,
y: baselineY,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
});
}
function _drawMultilineTextServer(page, text, rect, font) {
const lines = String(text).replace(/\r/g, '').split('\n');
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
lineHeight,
});
y -= lineHeight;
}
}
function _drawCombTextServer(page, text, rect, font) {
const chars = String(text).split('');
const count = Math.max(chars.length, 1);
const cellWidth = rect.width / count;
const fontSize = Math.max(8, Math.min(12, rect.height - 4));
chars.forEach((ch, i) => {
const textWidth = font.widthOfTextAtSize(ch, fontSize);
const x = rect.x + i * cellWidth + (cellWidth - textWidth) / 2;
const y = rect.y + Math.max(2, (rect.height - fontSize) / 2);
page.drawText(ch, {
x,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
});
if (i < count - 1) {
try {
page.drawLine({
start: { x: rect.x + (i + 1) * cellWidth, y: rect.y },
end: { x: rect.x + (i + 1) * cellWidth, y: rect.y + rect.height },
thickness: 0.4,
color: rgb(0.75, 0.75, 0.75),
});
} catch {
/* best effort */
}
}
});
}
function _fitSingleLineFontSizeServer(text, rect, font) {
let size = Math.min(12, rect.height - 4);
size = Math.max(size, 6);
while (size > 6) {
const width = font.widthOfTextAtSize(text, size);
if (width <= rect.width - 4) return size;
size -= 0.5;
}
return 6;
}
function _drawCheckBoxServer(page, field, rect, zapf) {
let checked = false;
try {
checked = field.isChecked();
} catch {
checked = false;
}
if (!checked) return;
const size = Math.max(8, Math.min(rect.width, rect.height) - 4);
page.drawText('\u2714', {
x: rect.x + Math.max(1, (rect.width - size * 0.7) / 2),
y: rect.y + Math.max(1, (rect.height - size) / 2),
size,
font: zapf,
color: rgb(0, 0, 0),
});
}
function _drawRadioGroupServer(page, field, widget, rect) {
let selected = null;
try {
selected = field.getSelected();
} catch {
selected = null;
}
if (!selected) return;
let widgetOnValue = null;
try {
widgetOnValue = widget.getOnValue?.();
} catch {
/* ignore */
}
if (!widgetOnValue) {
try {
const ap = widget.dict?.lookupMaybe?.(PDFName.of('AP'), PDFDict);
const n = ap?.lookupMaybe?.(PDFName.of('N'), PDFDict);
if (n) {
const keys = n.keys();
for (const k of keys) {
const name = k?.decodeText?.() ?? k?.encodedName ?? String(k);
if (name !== '/Off' && name !== 'Off') {
widgetOnValue = name.replace(/^\//, '');
break;
}
}
}
} catch {
/* ignore */
}
}
const selectedStr = String(selected).replace(/^\//, '');
const onStr = String(widgetOnValue ?? '').replace(/^\//, '');
if (!onStr || selectedStr !== onStr) return;
// Circle outline
try {
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: rect.width / 2 - 1,
yScale: rect.height / 2 - 1,
borderWidth: 0.8,
borderColor: rgb(0, 0, 0),
});
} catch {
/* best effort */
}
// Inner filled dot
try {
const r = Math.min(rect.width, rect.height) / 4;
page.drawEllipse({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2,
xScale: r,
yScale: r,
color: rgb(0, 0, 0),
});
} catch {
/* best effort */
}
}
function _drawDropdownServer(page, field, rect, font) {
let text = '';
try {
const selected = field.getSelected?.();
if (Array.isArray(selected)) {
text = selected.join(', ');
} else {
text = selected ?? '';
}
} catch {
text = '';
}
text = String(text ?? '');
if (!text) return;
const fontSize = _fitSingleLineFontSizeServer(text, rect, font);
page.drawText(text, {
x: rect.x + 2,
y: rect.y + Math.max(2, (rect.height - fontSize) / 2),
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 12),
});
}
function _drawOptionListServer(page, field, rect, font) {
let selected = [];
try {
selected = field.getSelected?.() || [];
} catch {
selected = [];
}
if (!Array.isArray(selected)) {
selected = [selected].filter(Boolean);
}
if (!selected.length) return;
const lines = selected.map(v => String(v));
const fontSize = Math.max(8, Math.min(11, rect.height / Math.max(lines.length + 0.5, 2)));
const lineHeight = fontSize + 1.5;
let y = rect.y + rect.height - fontSize - 2;
for (const line of lines) {
if (y < rect.y + 1) break;
page.drawText(line, {
x: rect.x + 2,
y,
size: fontSize,
font,
color: rgb(0, 0, 0),
maxWidth: Math.max(1, rect.width - 4),
});
y -= lineHeight;
}
}
/** Remove only Widget annotations; preserve links, stamps, comments, etc. */
function _removeWidgetAnnotationsServer(pdfDoc) {
for (const page of pdfDoc.getPages()) {
try {
const annotationsRef = page.node.get(PDFName.of('Annots'));
if (!annotationsRef) continue;
const annotations = pdfDoc.context.lookup(annotationsRef);
if (!annotations || !annotations.asArray) continue;
const filtered = annotations.asArray().filter(annotRef => {
try {
const annot = pdfDoc.context.lookup(annotRef);
const subtype = annot?.get(PDFName.of('Subtype'));
return subtype?.toString() !== '/Widget';
} catch {
return true;
}
});
if (filtered.length === 0) {
page.node.delete(PDFName.of('Annots'));
} else {
page.node.set(PDFName.of('Annots'), pdfDoc.context.obj(filtered));
}
} catch {
/* best effort */
}
}
try {
pdfDoc.catalog.delete(PDFName.of('AcroForm'));
} catch {
/* best effort */
}
}
// Format date and time for the selected timezone
export const formatTimeInTimezone = (date, timezone) => {
const nyDate = timezone && toZonedTime(date, timezone);
const generatedDate = timezone
? format(nyDate, 'EEE, dd MMM yyyy HH:mm:ss zzz', { timeZone: timezone })
: new Date(date).toUTCString();
return generatedDate;
};
// `getSecureUrl` is used to return local secure url if local files
export const getSecureUrl = url => {
const fileUrl = new URL(url)?.pathname?.includes('/files/');
if (fileUrl) {
try {
const file = getSignedLocalUrl(url);
if (file) {
return { url: file };
} else {
return { url: '' };
}
} catch (err) {
console.log('err while fileupload ', err);
return { url: '' };
}
} else {
return { url: url };
}
};
export const mailTemplate = param => {
const themeColor = '#47a3ad';
const subject = `${param.senderName} has requested you to sign "${param.title}"`;
const AppName = appName;
const logo = `<img src='https://qikinnovation.ams3.digitaloceanspaces.com/logo.png' height='50' />`;
const body =
"<html><head><meta http-equiv='Content-Type' content='text/html;charset=UTF-8' /></head><body><div style='background-color:#f5f5f5;padding:20px'><div style='background:white;padding-bottom:20px'><div style='padding:10px'>" +
logo +
`</div><div style='padding:2px;font-family:system-ui;background-color:${themeColor}'><p style='font-size:20px;font-weight:400;color:white;padding-left:20px'>Digital Signature Request</p></div><div><p style='padding:20px;font-size:14px;margin-bottom:10px'>` +
param.senderName +
' has requested you to review and sign <strong>' +
param.title +
"</strong>.</p><div style='padding: 5px 0px 5px 25px;display:flex;flex-direction:row;justify-content:space-around'><table><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Sender</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.senderMail +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Organization</td><td></td><td style='color:#626363;font-weight:bold'> " +
param.organization +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Expires on</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.localExpireDate +
"</td></tr><tr><td style='font-weight:bold;font-family:sans-serif;font-size:15px'>Note</td><td></td><td style='color:#626363;font-weight:bold'>" +
param.note +
"</td></tr><tr><td></td><td></td></tr></table></div> <div style='margin-left:70px'><a target=_blank href=" +
param.signingUrl +
"><button style='padding:12px;background-color:#d46b0f;color:white;border:0px;font-weight:bold;margin-top:30px'>Sign here</button></a></div><div style='display:flex;justify-content:center;margin-top:10px'></div></div></div><div><p> This is an automated email from " +
AppName +
'. For any queries regarding this email, please contact the sender ' +
param.senderMail +
` directly.</p></div></div></body></html>`;
return { subject, body };
};
export const selectFormat = data => {
switch (data) {
case 'L':
return 'MM/dd/yyyy';
case 'MM/DD/YYYY':
return 'MM/dd/yyyy';
case 'DD-MM-YYYY':
return 'dd-MM-yyyy';
case 'DD/MM/YYYY':
return 'dd/MM/yyyy';
case 'LL':
return 'MMMM dd, yyyy';
case 'DD MMM, YYYY':
return 'dd MMM, yyyy';
case 'YYYY-MM-DD':
return 'yyyy-MM-dd';
case 'MM-DD-YYYY':
return 'MM-dd-yyyy';
case 'MM.DD.YYYY':
return 'MM.dd.yyyy';
case 'MMM DD, YYYY':
return 'MMM dd, yyyy';
case 'MMMM DD, YYYY':
return 'MMMM dd, yyyy';
case 'DD MMMM, YYYY':
return 'dd MMMM, yyyy';
case 'DD.MM.YYYY':
return 'dd.MM.yyyy';
case 'DD-MMM-YYYY':
return 'dd-MMM-yyyy';
default:
return 'MM/dd/yyyy';
}
};
export function formatDateTime(date, dateFormat, timeZone, is12Hour) {
const zonedDate = toZonedTime(date, timeZone); // Convert date to the given timezone
const timeFormat = is12Hour ? 'hh:mm:ss a' : 'HH:mm:ss';
return dateFormat
? format(zonedDate, `${selectFormat(dateFormat)}, ${timeFormat} 'GMT' XXX`, { timeZone })
: formatTimeInTimezone(date, timeZone);
}
export const randomId = (digit = 8) => {
// 1. Grab a cryptographically-secure 32-bit random value
// Use crypto for stronger randomness
const randomBytes = crypto.getRandomValues(new Uint32Array(1));
const raw = randomBytes[0]; // 0 … 4,294,967,295
// Calculate the min and max for the given digit length
const min = Math.pow(10, digit - 1); // e.g., digit=3 → 100
const max = Math.pow(10, digit) - 1; // e.g., digit=3 → 999
const range = max - min + 1;
// Collapse random value into the range and shift
return min + (raw % range);
};
export const handleValidImage = async Placeholder => {
const updatedPlaceholders = [];
for (const placeholder of Placeholder || []) {
//Clean and format signerPtr
let signerPtr = placeholder.signerPtr;
// Check if signerPtr exists and has an id
if (signerPtr?.id) {
// Case 1: If signerPtr is a Parse Object instance
if (signerPtr instanceof Parse.Object) {
// If signerPtr has no attributes, its a plain pointer already
if (!signerPtr.attributes || Object.keys(signerPtr.attributes).length === 0) {
// Convert to a clean pointer using Parses built-in method
signerPtr = signerPtr.toPointer();
} else {
// If it has attributes, manually construct the pointer object
signerPtr = {
__type: 'Pointer',
className: signerPtr.className,
objectId: signerPtr.id,
};
}
// Case 2: If signerPtr is already a plain JS object resembling a pointer
} else if (typeof signerPtr === 'object' && signerPtr.className && signerPtr.objectId) {
// Normalize it to a valid Parse pointer object
signerPtr = {
__type: 'Pointer',
className: signerPtr.className,
objectId: signerPtr.objectId,
};
}
}
//Process placeHolder if Role is 'prefill'
if (placeholder?.Role === 'prefill') {
const updatedRole = [];
for (const item of placeholder.placeHolder || []) {
const updatedPos = [];
for (const posItem of item.pos || []) {
if (
(posItem?.type === 'image' || posItem?.type === 'draw') &&
posItem?.options?.response
) {
const validUrl = await getPresignedUrl(posItem?.options?.response);
updatedPos.push({
...posItem,
...(item.SignUrl !== undefined && { SignUrl: validUrl }),
options: { ...posItem.options, response: validUrl },
});
} else {
updatedPos.push(posItem);
}
}
updatedRole.push({ ...item, pos: updatedPos });
}
updatedPlaceholders.push({ ...placeholder, signerPtr, placeHolder: updatedRole });
} else {
// Not prefill role, just push as-is
updatedPlaceholders.push({ ...placeholder, signerPtr });
}
}
return updatedPlaceholders;
};