feat(Security): fixes
Some checks failed
Build and Push Docker Images (Default + Simple Mode) / build-and-release (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:simple suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:simple suffix:-simple]) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy static content to Pages / deploy (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile name:bentopdf]) (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile.nonroot name:bentopdf-nonroot]) (push) Has been cancelled
Trivy Security Scan / scan-dependencies (push) Has been cancelled
Trivy Security Scan / scan-config (push) Has been cancelled

This commit is contained in:
alam00000
2026-04-18 16:58:55 +05:30
parent b040aef729
commit 7527187812
8 changed files with 371 additions and 156 deletions

76
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"diff": "^8.0.3",
"dompurify": "^3.4.0",
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
"fast-xml-parser": "^5.7.1",
"heic2any": "^0.0.4",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",
@@ -2074,6 +2075,18 @@
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
"license": "ISC"
},
"node_modules/@nodable/entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nodable"
}
],
"license": "MIT"
},
"node_modules/@oxc-project/types": {
"version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
@@ -6993,6 +7006,42 @@
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/fast-xml-builder": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.1.3"
}
},
"node_modules/fast-xml-parser": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz",
"integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"@nodable/entities": "^2.1.0",
"fast-xml-builder": "^1.1.5",
"path-expression-matcher": "^1.5.0",
"strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -9861,6 +9910,21 @@
"node": ">=8"
}
},
"node_modules/path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -11347,6 +11411,18 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/strnum": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",

View File

@@ -94,6 +94,7 @@
"diff": "^8.0.3",
"dompurify": "^3.4.0",
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
"fast-xml-parser": "^5.7.1",
"heic2any": "^0.0.4",
"highlight.js": "^11.11.1",
"html2canvas": "^1.4.1",

View File

@@ -330,6 +330,22 @@ async function cacheInBatches(cache, urls, batchSize = 5) {
self.addEventListener('message', (event) => {
if (!event.data) return;
if (event.origin && event.origin !== self.location.origin) {
return;
}
const source = event.source;
if (source && typeof source === 'object' && 'url' in source) {
try {
const sourceOrigin = new URL(source.url).origin;
if (sourceOrigin !== self.location.origin) {
return;
}
} catch (e) {
return;
}
}
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
return;

View File

@@ -43,7 +43,26 @@ interface ErrorResult {
message: string;
}
self.onmessage = function (e: MessageEvent<WorkerMessage>) {
function isValidMessage(data: unknown): data is WorkerMessage {
if (!data || typeof data !== 'object') return false;
const m = data as Record<string, unknown>;
if (typeof m.id !== 'number') return false;
if (m.type === 'diff') {
return Array.isArray(m.beforeItems) && Array.isArray(m.afterItems);
}
if (m.type === 'pair') {
return Array.isArray(m.leftPages) && Array.isArray(m.rightPages);
}
return false;
}
self.onmessage = function (e: MessageEvent<unknown>) {
if (e.origin && e.origin !== '' && e.origin !== self.location.origin) {
return;
}
if (!isValidMessage(e.data)) {
return;
}
const msg = e.data;
try {
if (msg.type === 'diff') {

View File

@@ -720,7 +720,14 @@ async function handleMultiFileUpload(toolId: string) {
'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
const img = document.createElement('img');
img.src = url;
try {
const parsed = new URL(url);
if (parsed.protocol === 'blob:') {
img.src = parsed.href;
}
} catch {
console.warn('Invalid blob URL for preview');
}
img.className = 'max-w-full max-h-full object-contain';
const p = document.createElement('p');

View File

@@ -203,9 +203,16 @@ function initializePage(): void {
if (sigImageThumb && sigImagePreview) {
const url = URL.createObjectURL(file);
sigImageThumb.src = url;
try {
const parsed = new URL(url);
if (parsed.protocol === 'blob:') {
sigImageThumb.src = parsed.href;
sigImagePreview.classList.remove('hidden');
}
} catch {
console.warn('Invalid blob URL for signature preview');
}
}
}
});
}

View File

@@ -19,6 +19,7 @@ type LucideWindow = Window & {
};
};
import DOMPurify from 'dompurify';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
@@ -1327,7 +1328,7 @@ function showProperties(field: FormField): void {
`;
}
propertiesPanel.innerHTML = `
const propertiesHtml = `
<div class="space-y-3">
<div>
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
@@ -1399,6 +1400,10 @@ function showProperties(field: FormField): void {
</div>
`;
propertiesPanel.innerHTML = DOMPurify.sanitize(propertiesHtml, {
ADD_ATTR: ['target'],
});
// Common listeners
const propName = document.getElementById('propName') as HTMLInputElement;
const nameError = document.getElementById('nameError') as HTMLDivElement;

View File

@@ -1,5 +1,6 @@
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
import { XMLParser } from 'fast-xml-parser';
export interface XmlToPdfOptions {
onProgress?: (percent: number, message: string) => void;
@@ -9,6 +10,9 @@ interface jsPDFWithAutoTable extends jsPDF {
lastAutoTable?: { finalY: number };
}
const ATTR_PREFIX = '@_';
const TEXT_KEY = '#text';
export async function convertXmlToPdf(
file: File,
options?: XmlToPdfOptions
@@ -16,48 +20,82 @@ export async function convertXmlToPdf(
const { onProgress } = options || {};
onProgress?.(10, 'Reading XML file...');
const xmlText = await file.text();
const rawXmlText = await file.text();
const xmlText = String(rawXmlText)
.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
.replace(/<!ENTITY[\s\S]*?>/gi, '')
.replace(/<\?xml-stylesheet[\s\S]*?\?>/gi, '');
onProgress?.(30, 'Parsing XML structure...');
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'text/xml');
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: ATTR_PREFIX,
textNodeName: TEXT_KEY,
allowBooleanAttributes: true,
parseTagValue: false,
parseAttributeValue: false,
trimValues: true,
processEntities: true,
ignoreDeclaration: true,
ignorePiTags: true,
});
const parseError = xmlDoc.querySelector('parsererror');
if (parseError) {
throw new Error('Invalid XML: ' + parseError.textContent);
let parsed: Record<string, unknown>;
try {
parsed = parser.parse(xmlText) as Record<string, unknown>;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error('Invalid XML: ' + toSafeText(msg), { cause: err });
}
const rootKeys = Object.keys(parsed);
if (rootKeys.length === 0) {
throw new Error('Invalid XML: no root element');
}
const rootName = rootKeys[0];
const rootValue = parsed[rootName];
onProgress?.(50, 'Analyzing data structure...');
const doc: jsPDFWithAutoTable = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
format: 'a4',
});
const pageWidth = doc.internal.pageSize.getWidth();
let yPosition = 20;
const root = xmlDoc.documentElement;
const rootName = formatTitle(root.tagName);
doc.setFontSize(18);
doc.setFont('helvetica', 'bold');
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' });
doc.text(formatTitle(rootName), pageWidth / 2, yPosition, {
align: 'center',
});
yPosition += 15;
onProgress?.(60, 'Generating formatted content...');
const children = Array.from(root.children);
if (isPlainObject(rootValue)) {
const rootObj = rootValue;
const childEntries = Object.entries(rootObj).filter(
([k]) => !k.startsWith(ATTR_PREFIX) && k !== TEXT_KEY
);
if (children.length > 0) {
const groups = groupByTagName(children);
if (childEntries.length > 0) {
const groups = groupChildrenByTagName(childEntries);
const renderableGroups: Array<[string, Record<string, unknown>[]]> = [];
for (const [groupName, elements] of Object.entries(groups)) {
const { headers, rows } = extractTableData(elements);
if (headers.length > 0 && rows.length > 0) {
if (Object.keys(groups).length > 1) {
renderableGroups.push([groupName, elements]);
}
}
for (const [groupName, elements] of renderableGroups) {
const { headers, rows } = extractTableData(elements);
if (renderableGroups.length > 1) {
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(formatTitle(groupName), 14, yPosition);
@@ -65,7 +103,7 @@ export async function convertXmlToPdf(
}
autoTable(doc, {
head: [headers.map(h => formatTitle(h))],
head: [headers.map((h) => formatTitle(h))],
body: rows,
startY: yPosition,
styles: {
@@ -85,14 +123,13 @@ export async function convertXmlToPdf(
theme: 'striped',
didDrawPage: (data) => {
yPosition = (data.cursor?.y || yPosition) + 10;
}
},
});
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
}
}
} else {
const kvPairs = extractKeyValuePairs(root);
const kvPairs = extractKeyValuePairs(rootObj);
if (kvPairs.length > 0) {
autoTable(doc, {
head: [['Property', 'Value']],
@@ -116,6 +153,7 @@ export async function convertXmlToPdf(
});
}
}
}
onProgress?.(90, 'Finalizing PDF...');
@@ -125,40 +163,48 @@ export async function convertXmlToPdf(
return pdfBlob;
}
function isPlainObject(val: unknown): val is Record<string, unknown> {
return val !== null && typeof val === 'object' && !Array.isArray(val);
}
function groupByTagName(elements: Element[]): Record<string, Element[]> {
const groups: Record<string, Element[]> = {};
for (const element of elements) {
const tagName = element.tagName;
if (!groups[tagName]) {
groups[tagName] = [];
function groupChildrenByTagName(
entries: [string, unknown][]
): Record<string, Record<string, unknown>[]> {
const groups: Record<string, Record<string, unknown>[]> = {};
for (const [tagName, value] of entries) {
const items = Array.isArray(value) ? value : [value];
const normalized: Record<string, unknown>[] = items.map((v) => {
if (isPlainObject(v)) return v;
if (v == null) return {};
return { [TEXT_KEY]: String(v) };
});
groups[tagName] = normalized;
}
groups[tagName].push(element);
}
return groups;
}
function extractTableData(elements: Element[]): { headers: string[], rows: string[][] } {
if (elements.length === 0) {
return { headers: [], rows: [] };
}
function extractTableData(elements: Record<string, unknown>[]): {
headers: string[];
rows: string[][];
} {
if (elements.length === 0) return { headers: [], rows: [] };
const headerSet = new Set<string>();
for (const element of elements) {
for (const child of Array.from(element.children)) {
headerSet.add(child.tagName);
for (const key of Object.keys(element)) {
if (key.startsWith(ATTR_PREFIX)) continue;
if (key === TEXT_KEY) continue;
headerSet.add(key);
}
}
const headers = Array.from(headerSet);
if (headers.length === 0) return { headers: [], rows: [] };
const rows: string[][] = [];
for (const element of elements) {
const row: string[] = [];
for (const header of headers) {
const child = element.querySelector(header);
row.push(child?.textContent?.trim() || '');
row.push(toSafeText(stringifyValue(element[header])));
}
rows.push(row);
}
@@ -166,31 +212,69 @@ function extractTableData(elements: Element[]): { headers: string[], rows: strin
return { headers, rows };
}
function extractKeyValuePairs(element: Element): string[][] {
function extractKeyValuePairs(obj: Record<string, unknown>): string[][] {
const pairs: string[][] = [];
for (const child of Array.from(element.children)) {
const key = child.tagName;
const value = child.textContent?.trim() || '';
if (value) {
pairs.push([formatTitle(key), value]);
for (const [key, val] of Object.entries(obj)) {
if (key.startsWith(ATTR_PREFIX)) continue;
if (key === TEXT_KEY) continue;
const strVal = toSafeText(stringifyValue(val));
if (strVal) {
pairs.push([formatTitle(key), strVal]);
}
}
for (const attr of Array.from(element.attributes)) {
pairs.push([formatTitle(attr.name), attr.value]);
for (const [key, val] of Object.entries(obj)) {
if (!key.startsWith(ATTR_PREFIX)) continue;
const attrName = key.slice(ATTR_PREFIX.length);
pairs.push([formatTitle(attrName), toSafeText(stringifyValue(val))]);
}
return pairs;
}
function stringifyValue(val: unknown): string {
if (val == null) return '';
if (typeof val === 'string') return val;
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
if (Array.isArray(val)) {
return val
.map((v) => stringifyValue(v))
.filter((s) => s.length > 0)
.join(', ');
}
if (isPlainObject(val)) {
const parts: string[] = [];
if (TEXT_KEY in val) {
const t = stringifyValue(val[TEXT_KEY]);
if (t) parts.push(t);
}
for (const [k, v] of Object.entries(val)) {
if (k.startsWith(ATTR_PREFIX)) continue;
if (k === TEXT_KEY) continue;
const inner = stringifyValue(v);
if (inner) parts.push(inner);
}
return parts.join(' ');
}
return String(val);
}
function toSafeText(raw: string | null | undefined): string {
if (raw == null) return '';
return (
String(raw)
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
.trim()
);
}
function formatTitle(tagName: string): string {
return tagName
.replace(/[_-]/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
}