banisterious_obsidian-chart.../patch-jspdf.js
John Banister 4b4dd70393 fix(security): Strip pdfobjectnewwindow output mode from jspdf bundle
jspdf's output('pdfobjectnewwindow') dynamically loads pdfobject.min.js from a CDN by creating a <script> element. It's the only createElement('script') call in jspdf's source. The plugin only calls save(), so the case body is unreachable; the v0.22.56 Community Plugins automated review flagged the dynamic script element creation as an error.

A new patch-jspdf.js postinstall script replaces the case body with a throw, preserving the switch shape so jspdf still parses cleanly while dropping the dynamic script element creation from the bundle. Hooked into the postinstall chain alongside the existing patches for family-chart, leaflet-distortable, core-js, and pdfmake. eslint.config.mjs adds patch-jspdf.js to the ignore list to match the convention.
2026-05-29 15:43:03 -07:00

115 lines
4.9 KiB
JavaScript

/**
* Postinstall patch: strip the `pdfobjectnewwindow` output mode from
* jspdf's bundled ES build. That branch dynamically loads
* `pdfobject.min.js` from a CloudFlare CDN by creating a `<script>`
* element and appending it to a new-window document — the only
* `document.createElement("script")` call in jspdf's source.
*
* Obsidian's Community automated review flags the bundled main.js for
* dynamic script element creation when the unreachable branch survives
* the bundle. Charted Roots only calls jspdf's `save()` (Family Chart
* export, Tree wizard PDF output); the `pdfobjectnewwindow` and
* `pdfjsnewwindow` output modes are never reached.
*
* Replacing the case body with a throw preserves the switch shape so
* jspdf still parses cleanly, drops the dynamic script element creation
* from the bundle, and surfaces a clear error if a future call site
* does request the removed mode.
*
* The bundler uses `dist/jspdf.es.min.js` per jspdf's `browser` and
* `module` package.json entries, so the minified file is the primary
* target. The unminified `dist/jspdf.es.js` is patched in parallel so
* source-map debugging stays consistent with runtime.
*
* Each substitution carries an idempotency marker so re-running the
* patch is a no-op. If an ORIGINAL string isn't found (vendor update),
* the patch logs a warning and skips that substitution; it never
* silently mis-edits.
*/
const fs = require('fs');
const path = require('path');
const MARKER = 'charted-roots-postinstall-patch';
const TARGETS = [
{
name: 'jspdf.es.min.js (minified browser entry)',
path: path.join(__dirname, 'node_modules', 'jspdf', 'dist', 'jspdf.es.min.js'),
original:
'case"pdfobjectnewwindow":if("[object Window]"===Object.prototype.toString.call(i)){var a="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js",s=!e.pdfObjectUrl;s||(a=e.pdfObjectUrl);var u=i.open();if(null!==u){var c=Ie(u),l=c.document.createElement("script"),h=this;l.src=a,s&&(l.integrity="sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==",l.crossOrigin="anonymous"),l.onload=function(){u.PDFObject.embed(h.output("dataurlstring"),e)},c.body.appendChild(l)}return u}throw new Error("The option pdfobjectnewwindow just works in a browser-environment.");',
patched:
'case"pdfobjectnewwindow":throw new Error("pdfobjectnewwindow output mode removed by ' +
MARKER +
' (dynamic script loading not supported in this build)");',
},
{
name: 'jspdf.es.js (unminified ES module)',
path: path.join(__dirname, 'node_modules', 'jspdf', 'dist', 'jspdf.es.js'),
original: ` case "pdfobjectnewwindow":
if (Object.prototype.toString.call(globalObject) === "[object Window]") {
var pdfObjectUrl = "https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.1.1/pdfobject.min.js";
var useDefaultPdfObjectUrl = !options.pdfObjectUrl;
if (!useDefaultPdfObjectUrl) {
pdfObjectUrl = options.pdfObjectUrl;
}
var nW = globalObject.open();
if (nW !== null) {
var initializedPdfObjectWindow = initializeNewWindow(nW);
var pdfObjectScript = initializedPdfObjectWindow.document.createElement("script");
var scope = this;
pdfObjectScript.src = pdfObjectUrl;
if (useDefaultPdfObjectUrl) {
pdfObjectScript.integrity = "sha512-4ze/a9/4jqu+tX9dfOqJYSvyYd5M6qum/3HpCLr+/Jqf0whc37VUbkpNGHR7/8pSnCFw47T1fmIpwBV7UySh3g==";
pdfObjectScript.crossOrigin = "anonymous";
}
pdfObjectScript.onload = function () {
nW.PDFObject.embed(scope.output("dataurlstring"), options);
};
initializedPdfObjectWindow.body.appendChild(pdfObjectScript);
}
return nW;
} else {
throw new Error("The option pdfobjectnewwindow just works in a browser-environment.");
}`,
patched: ` case "pdfobjectnewwindow":
// pdfobjectnewwindow output mode removed by ${MARKER}
// (dynamic script loading not supported in this build)
throw new Error("pdfobjectnewwindow output mode removed by ${MARKER}");`,
},
];
let totalWarnings = 0;
for (const t of TARGETS) {
if (!fs.existsSync(t.path)) {
console.log(`[patch-jspdf] ${t.name}: file not found; skipping.`);
continue;
}
let source = fs.readFileSync(t.path, 'utf8');
if (source.includes(t.patched)) {
console.log(`[patch-jspdf] ${t.name}: already patched.`);
continue;
}
if (!source.includes(t.original)) {
console.warn(
`[patch-jspdf] ${t.name}: ORIGINAL string not found. ` +
'Vendor may have updated; skipping this substitution.'
);
totalWarnings += 1;
continue;
}
source = source.replace(t.original, t.patched);
fs.writeFileSync(t.path, source, 'utf8');
console.log(`[patch-jspdf] ${t.name}: patched.`);
}
if (totalWarnings > 0) {
console.warn(
`[patch-jspdf] ${totalWarnings} substitution(s) skipped; review jspdf's release notes.`
);
}