| const Busboy = require('busboy');
|
| const JSZip = require('jszip');
|
| const { applyCorsHeaders, handleCorsPreflight } = require('../lib/cors-middleware');
|
|
|
|
|
| function sendJson(res, status, data) {
|
| res.setHeader('Content-Type', 'application/json');
|
| res.status(status).end(JSON.stringify(data));
|
| }
|
|
|
| module.exports = async (req, res) => {
|
| if (handleCorsPreflight(req, res, { allowedMethods: 'POST, OPTIONS' })) {
|
| return;
|
| }
|
| applyCorsHeaders(req, res, { allowedMethods: 'POST, OPTIONS' });
|
|
|
| if (req.method !== 'POST') {
|
| sendJson(res, 405, { error: 'Method not allowed' });
|
| return;
|
| }
|
|
|
| try {
|
| const busboy = Busboy({ headers: req.headers });
|
| let fileData = null;
|
| let filename = null;
|
|
|
| busboy.on('file', (fieldname, file, info) => {
|
| filename = info.filename;
|
| const chunks = [];
|
|
|
| file.on('data', (chunk) => {
|
| chunks.push(chunk);
|
| });
|
|
|
| file.on('end', () => {
|
| fileData = Buffer.concat(chunks);
|
| });
|
| });
|
|
|
| busboy.on('finish', async () => {
|
| if (!fileData || !filename) {
|
| res.status(400).json({ error: 'No file uploaded' });
|
| return;
|
| }
|
|
|
| if (!filename.toLowerCase().endsWith('.docx')) {
|
| res.status(400).json({ error: 'Please upload a .docx file' });
|
| return;
|
| }
|
|
|
| try {
|
| const remediatedFile = await remediateDocx(fileData, filename);
|
|
|
|
|
| let suggestedName = filename
|
| .replace(/_/g, '-')
|
| .replace(/\.docx$/i, '-remediated.docx');
|
|
|
| res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
| res.setHeader('Content-Disposition', `attachment; filename="${suggestedName}"`);
|
| res.status(200).send(remediatedFile);
|
|
|
| } catch (error) {
|
| res.status(500).json({ error: error.message });
|
| }
|
| });
|
|
|
| req.pipe(busboy);
|
|
|
| } catch (error) {
|
| res.status(500).json({ error: error.message });
|
| }
|
| };
|
|
|
| async function remediateDocx(fileData, filename) {
|
| try {
|
| const zip = await JSZip.loadAsync(fileData);
|
|
|
|
|
| const writeIfChanged = (filename, original, modified) => {
|
| if (original !== modified && modified !== null) {
|
| zip.file(filename, modified);
|
| return true;
|
| }
|
| return false;
|
| };
|
|
|
|
|
| const docFile = zip.file('word/document.xml');
|
| if (docFile) {
|
| const origDocXml = await docFile.async('string');
|
| const afterShadows = removeShadowsOnly(origDocXml);
|
| const afterInlineContent = applyInlineContentFixes(afterShadows || origDocXml);
|
| writeIfChanged('word/document.xml', origDocXml, afterInlineContent);
|
| }
|
|
|
|
|
| const stylesFile = zip.file('word/styles.xml');
|
| if (stylesFile) {
|
| const origStylesXml = await stylesFile.async('string');
|
| const afterStylesShadows = removeShadowsOnly(origStylesXml);
|
| writeIfChanged('word/styles.xml', origStylesXml, afterStylesShadows);
|
| }
|
|
|
|
|
| const themeFile = zip.file('word/theme/theme1.xml');
|
| if (themeFile) {
|
| const origThemeXml = await themeFile.async('string');
|
| const afterTheme = removeShadowsOnly(origThemeXml);
|
| writeIfChanged('word/theme/theme1.xml', origThemeXml, afterTheme);
|
| }
|
|
|
|
|
| try {
|
| const settingsFile = zip.file('word/settings.xml');
|
| if (settingsFile) {
|
| const origSettings = await settingsFile.async('string');
|
| const hasAnyProt = /<w:(?:documentProtection|writeProtection|readOnlyRecommended|editRestrictions|formProtection|protection|docProtection|enforcement|locked|trackRevisions|crypt)\b/.test(origSettings);
|
| if (hasAnyProt) {
|
| let cleaned = origSettings;
|
|
|
| cleaned = cleaned.replace(/<w:(?:documentProtection|writeProtection|readOnlyRecommended|editRestrictions|formProtection|protection|docProtection)[^>]*\/>/g, '');
|
| cleaned = cleaned.replace(/<w:(?:documentProtection|writeProtection|readOnlyRecommended|editRestrictions|formProtection|protection|docProtection)[^>]*>[\s\S]*?<\/w:(?:documentProtection|writeProtection|readOnlyRecommended|editRestrictions|formProtection|protection|docProtection)>/g, '');
|
| cleaned = cleaned.replace(/<w:(?:enforcement|locked|trackRevisions)[^>]*\/>/g, '');
|
| cleaned = cleaned.replace(/<w:(?:enforcement|locked|trackRevisions)[^>]*>[\s\S]*?<\/w:(?:enforcement|locked|trackRevisions)>/g, '');
|
| cleaned = cleaned.replace(/<w:crypt[^>]*\/>/g, '');
|
| cleaned = cleaned.replace(/<w:crypt[^>]*>[\s\S]*?<\/w:crypt[^>]*>/g, '');
|
| cleaned = cleaned.replace(/\s?w:(?:locked|trackRevisions|enforcement)="[^"]*"/g, '');
|
|
|
| writeIfChanged('word/settings.xml', origSettings, cleaned);
|
| }
|
| }
|
| } catch (e) {
|
| console.warn('[remediateDocx] Protection removal failed:', e.message);
|
| }
|
|
|
|
|
| const remediatedBuffer = await zip.generateAsync({
|
| type: 'nodebuffer',
|
| compression: 'DEFLATE',
|
| compressionOptions: { level: 6 }
|
| });
|
|
|
| return remediatedBuffer;
|
|
|
| } catch (error) {
|
| throw new Error(`Failed to remediate document: ${error.message}`);
|
| }
|
| }
|
|
|
|
|
|
|
| function applyInlineContentFixes(xmlContent) {
|
| if (!xmlContent) return null;
|
|
|
| const original = xmlContent;
|
| let fixedXml = xmlContent;
|
|
|
|
|
| const floatingPatterns = [
|
|
|
| {
|
| pattern: /<wp:anchor[^>]*>([\s\S]*?)<\/wp:anchor>/g,
|
| replacement: function(match, content) {
|
|
|
| return `<wp:inline>${content}</wp:inline>`;
|
| }
|
| },
|
|
|
| {
|
| pattern: /<wp:wrapSquare[^>]*\/>/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /<wp:wrapTight[^>]*>[\s\S]*?<\/wp:wrapTight>/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /<wp:wrapThrough[^>]*>[\s\S]*?<\/wp:wrapThrough>/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /<wp:wrapTopAndBottom[^>]*\/>/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /<wp:wrapNone[^>]*\/>/g,
|
| replacement: ''
|
| },
|
|
|
| {
|
| pattern: /<wp:positionH[^>]*>[\s\S]*?<\/wp:positionH>/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /<wp:positionV[^>]*>[\s\S]*?<\/wp:positionV>/g,
|
| replacement: ''
|
| },
|
|
|
| {
|
| pattern: /mso-position-horizontal:[^;]*;?/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /mso-position-vertical:[^;]*;?/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /mso-wrap-style:[^;]*;?/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /left:\s*[^;]*;?/g,
|
| replacement: ''
|
| },
|
| {
|
| pattern: /top:\s*[^;]*;?/g,
|
| replacement: ''
|
| }
|
| ];
|
|
|
|
|
| floatingPatterns.forEach(patternObj => {
|
| const { pattern, replacement } = patternObj;
|
|
|
| if (typeof replacement === 'function') {
|
| fixedXml = fixedXml.replace(pattern, replacement);
|
| } else {
|
| fixedXml = fixedXml.replace(pattern, replacement);
|
| }
|
| });
|
|
|
|
|
| const drawingPattern = /<w:drawing[^>]*>[\s\S]*?<\/w:drawing>/g;
|
| const drawingMatches = fixedXml.match(drawingPattern);
|
|
|
| if (drawingMatches) {
|
| drawingMatches.forEach(drawing => {
|
|
|
| if (drawing.includes('wp:anchor') && !drawing.includes('wp:inline')) {
|
|
|
| let fixedDrawing = drawing.replace(/<wp:anchor[^>]*>/g, '<wp:inline>');
|
| fixedDrawing = fixedDrawing.replace(/<\/wp:anchor>/g, '</wp:inline>');
|
|
|
| if (fixedDrawing !== drawing) {
|
| fixedXml = fixedXml.replace(drawing, fixedDrawing);
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| if (fixedXml === original) return null;
|
| return fixedXml;
|
| }
|
|
|
| function removeShadowsOnly(xmlContent) {
|
| const original = xmlContent;
|
| let fixedXml = xmlContent;
|
|
|
|
|
| fixedXml = fixedXml.replace(/<w:shadow\s*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w:shadow[^>]*>.*?<\/w:shadow>/g, '');
|
| fixedXml = fixedXml.replace(/\s+\w*shadow\w*\s*=\s*"[^"]*"/g, '');
|
|
|
|
|
| fixedXml = fixedXml.replace(/<a:outerShdw[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<a:outerShdw[^>]*>.*?<\/a:outerShdw>/g, '');
|
| fixedXml = fixedXml.replace(/<a:innerShdw[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<a:innerShdw[^>]*>.*?<\/a:innerShdw>/g, '');
|
| fixedXml = fixedXml.replace(/<a:prstShdw[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<a:prstShdw[^>]*>.*?<\/a:prstShdw>/g, '');
|
|
|
|
|
| fixedXml = fixedXml.replace(/<w14:shadow[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:shadow[^>]*>.*?<\/w14:shadow>/g, '');
|
| fixedXml = fixedXml.replace(/<w15:shadow[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w15:shadow[^>]*>.*?<\/w15:shadow>/g, '');
|
|
|
|
|
| fixedXml = fixedXml.replace(/<w14:glow[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:glow[^>]*>.*?<\/w14:glow>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:reflection[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:reflection[^>]*>.*?<\/w14:reflection>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:props3d[^>]*\/>/g, '');
|
| fixedXml = fixedXml.replace(/<w14:props3d[^>]*>.*?<\/w14:props3d>/g, '');
|
|
|
|
|
|
|
| fixedXml = fixedXml.replace(/\s+\w*shdw\w*\s*=\s*"[^"]*"/g, '');
|
|
|
|
|
|
|
|
|
|
|
| if (fixedXml === original) return null;
|
| return fixedXml;
|
| }
|
|
|