diff --git a/src/pdf.sandbox.js b/src/pdf.sandbox.js index caec9dd24..cda7e4380 100644 --- a/src/pdf.sandbox.js +++ b/src/pdf.sandbox.js @@ -58,6 +58,9 @@ class Sandbox { } create(data) { + if (TESTING) { + this._module.ccall("nukeSandbox", null, []); + } const sandboxData = JSON.stringify(data); const code = [ // Next line is replaced by code from initialization.js diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js index 9165a01ac..3686ed9b4 100644 --- a/src/scripting_api/aform.js +++ b/src/scripting_api/aform.js @@ -60,10 +60,7 @@ class AForm { } } - AFMergeChange(event) { - if (!event) { - event = this._document._event; - } + AFMergeChange(event = globalThis.event) { if (event.willCommit) { return event.value.toString(); } @@ -128,7 +125,7 @@ class AForm { strCurrency, bCurrencyPrepend ) { - const event = this._document._event; + const event = globalThis.event; if (!event.value) { return; } @@ -193,7 +190,7 @@ class AForm { strCurrency /* unused */, bCurrencyPrepend /* unused */ ) { - const event = this._document._event; + const event = globalThis.event; let value = this.AFMergeChange(event); if (!value) { return; @@ -236,7 +233,7 @@ class AForm { throw new Error("Invalid nDec value in AFPercent_Format"); } - const event = this._document._event; + const event = globalThis.event; if (nDec > 512) { event.value = "%"; return; @@ -268,7 +265,7 @@ class AForm { } AFDate_FormatEx(cFormat) { - const event = this._document._event; + const event = globalThis.event; const value = event.value; if (!value) { return; @@ -287,7 +284,7 @@ class AForm { } AFDate_KeystrokeEx(cFormat) { - const event = this._document._event; + const event = globalThis.event; if (!event.willCommit) { return; } @@ -310,7 +307,7 @@ class AForm { } AFRange_Validate(bGreaterThan, nGreaterThan, bLessThan, nLessThan) { - const event = this._document._event; + const event = globalThis.event; if (!event.value) { return; } @@ -397,7 +394,7 @@ class AForm { throw new TypeError("Invalid function in AFSimple_Calculate"); } - const event = this._document._event; + const event = globalThis.event; const values = []; for (const cField of cFields) { const field = this._document.getField(cField); @@ -417,7 +414,7 @@ class AForm { } AFSpecial_Format(psf) { - const event = this._document._event; + const event = globalThis.event; if (!event.value) { return; } @@ -457,7 +454,7 @@ class AForm { return; } - const event = this._document._event; + const event = globalThis.event; const value = this.AFMergeChange(event); const checkers = new Map([ ["9", char => char >= "0" && char <= "9"], @@ -526,7 +523,7 @@ class AForm { } AFSpecial_Keystroke(psf) { - const event = this._document._event; + const event = globalThis.event; if (!event.value) { return; } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 977252541..864295532 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -31,17 +31,21 @@ class Doc extends PDFObject { constructor(data) { super(data); - this.baseURL = data.baseURL || ""; - this.calculate = true; - this.delay = false; - this.dirty = false; - this.disclosed = false; - this.media = undefined; - this.metadata = data.metadata; - this.noautocomplete = undefined; - this.nocache = undefined; - this.spellDictionaryOrder = []; - this.spellLanguageOrder = []; + // In a script doc === this. + // So adding a property to the doc means adding it to this + this._expandos = globalThis; + + this._baseURL = data.baseURL || ""; + this._calculate = true; + this._delay = false; + this._dirty = false; + this._disclosed = false; + this._media = undefined; + this._metadata = data.metadata; + this._noautocomplete = undefined; + this._nocache = undefined; + this._spellDictionaryOrder = []; + this._spellLanguageOrder = []; this._printParams = null; this._fields = new Map(); @@ -127,6 +131,14 @@ class Doc extends PDFObject { throw new Error("doc.author is read-only"); } + get baseURL() { + return this._baseURL; + } + + set baseURL(baseURL) { + this._baseURL = baseURL; + } + get bookmarkRoot() { return undefined; } @@ -135,6 +147,14 @@ class Doc extends PDFObject { throw new Error("doc.bookmarkRoot is read-only"); } + get calculate() { + return this._calculate; + } + + set calculate(calculate) { + this._calculate = calculate; + } + get creator() { return this._creator; } @@ -151,6 +171,30 @@ class Doc extends PDFObject { throw new Error("doc.dataObjects is read-only"); } + get delay() { + return this._delay; + } + + set delay(delay) { + this._delay = delay; + } + + get dirty() { + return this._dirty; + } + + set dirty(dirty) { + this._dirty = dirty; + } + + get disclosed() { + return this._disclosed; + } + + set disclosed(disclosed) { + this._disclosed = disclosed; + } + get docID() { return this._docID; } @@ -278,6 +322,22 @@ class Doc extends PDFObject { this._layout = value; } + get media() { + return this._media; + } + + set media(media) { + this._media = media; + } + + get metadata() { + return this._metadata; + } + + set metadata(metadata) { + this._metadata = metadata; + } + get modDate() { return this._modDate; } @@ -302,6 +362,22 @@ class Doc extends PDFObject { throw new Error("doc.mouseY is read-only"); } + get noautocomplete() { + return this._noautocomplete; + } + + set noautocomplete(noautocomplete) { + this._noautocomplete = noautocomplete; + } + + get nocache() { + return this._nocache; + } + + set nocache(nocache) { + this._nocache = nocache; + } + get numFields() { return this._numFields; } @@ -418,6 +494,22 @@ class Doc extends PDFObject { throw new Error("doc.sounds is read-only"); } + get spellDictionaryOrder() { + return this._spellDictionaryOrder; + } + + set spellDictionaryOrder(spellDictionaryOrder) { + this._spellDictionaryOrder = spellDictionaryOrder; + } + + get spellLanguageOrder() { + return this._spellLanguageOrder; + } + + set spellLanguageOrder(spellLanguageOrder) { + this._spellLanguageOrder = spellLanguageOrder; + } + get subject() { return this._subject; } diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index 542f16121..67ef096b8 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -70,7 +70,7 @@ class EventDispatcher { const name = baseEvent.name.replace(" ", ""); const source = this._objects[id]; - const event = (this._document.obj._event = new Event(baseEvent)); + globalThis.event = new Event(baseEvent); let savedChange; if (source.obj._isButton()) { @@ -155,7 +155,7 @@ class EventDispatcher { } const first = this._calculationOrder[0]; const source = this._objects[first]; - const event = (this._document.obj._event = new Event({})); + globalThis.event = new Event({}); this.runCalculate(source, event); } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 915accd3d..46f100a2f 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -77,6 +77,8 @@ class Field extends PDFObject { this._fillColor = data.fillColor || ["T"]; this._strokeColor = data.strokeColor || ["G", 0]; this._textColor = data.textColor || ["G", 0]; + + this._globalEval = data.globalEval; } get fillColor() { @@ -117,20 +119,6 @@ class Field extends PDFObject { this._valueAsString = val ? val.toString() : ""; } - _getFunction(code, actionName) { - try { - // This eval is running in a sandbox so it's safe to use Function - // eslint-disable-next-line no-new-func - return Function("event", `with (this) {${code}}`).bind(this._document); - } catch (error) { - const value = - `"${error.toString()}" for action ` + - `"${actionName}" in object ${this._id}.`; - this._send({ command: "error", value }); - } - return null; - } - setAction(cTrigger, cScript) { if (typeof cTrigger !== "string" || typeof cScript !== "string") { return; @@ -138,10 +126,7 @@ class Field extends PDFObject { if (!(cTrigger in this._actions)) { this._actions[cTrigger] = []; } - const fun = this._getFunction(cScript, cTrigger); - if (fun) { - this._actions[cTrigger].push(fun); - } + this._actions[cTrigger].push(cScript); } setFocus() { @@ -152,12 +137,7 @@ class Field extends PDFObject { const actionsMap = new Map(); if (actions) { for (const [eventType, actionsForEvent] of Object.entries(actions)) { - const functions = actionsForEvent - .map(action => this._getFunction(action, eventType)) - .filter(fun => !!fun); - if (functions.length > 0) { - actionsMap.set(eventType, functions); - } + actionsMap.set(eventType, actionsForEvent); } } return actionsMap; @@ -176,7 +156,8 @@ class Field extends PDFObject { const actions = this._actions.get(eventName); try { for (const action of actions) { - action(event); + // Action evaluation must happen in the global scope + this._globalEval(action); } } catch (error) { event.rc = false; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 661e4bae4..3be859a18 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -72,6 +72,7 @@ function initSandbox(params) { obj.send = send; obj.globalEval = globalEval; obj.doc = _document.wrapped; + obj.globalEval = globalEval; const field = new Field(obj); const wrapped = new Proxy(field, proxyHandler); doc._addField(name, wrapped); @@ -81,7 +82,6 @@ function initSandbox(params) { globalThis.event = null; globalThis.global = Object.create(null); globalThis.app = new Proxy(app, proxyHandler); - globalThis.doc = _document.wrapped; globalThis.color = new Proxy(new Color(), proxyHandler); globalThis.console = new Proxy(new Console({ send }), proxyHandler); globalThis.util = new Proxy(util, proxyHandler); @@ -103,6 +103,26 @@ function initSandbox(params) { } } + // The doc properties must live in the global scope too + const properties = Object.create(null); + for (const name of Object.getOwnPropertyNames(Doc.prototype)) { + if (name === "constructor" || name.startsWith("_")) { + continue; + } + const descriptor = Object.getOwnPropertyDescriptor(Doc.prototype, name); + if (descriptor.get) { + properties[name] = { + get: descriptor.get.bind(doc), + set: descriptor.set.bind(doc), + }; + } else { + properties[name] = { + value: Doc.prototype[name].bind(doc), + }; + } + } + Object.defineProperties(globalThis, properties); + const functions = { dispatchEvent: app._dispatchEvent.bind(app), timeoutCb: app._evalCallback.bind(app), diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 5973da0fc..c4991e338 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -129,6 +129,38 @@ describe("Scripting", function () { }); }); + describe("Doc", function () { + it("should treat globalThis as the doc", async function (done) { + const refId = getId(); + const data = { + objects: { + field: [ + { + id: refId, + value: "", + actions: {}, + type: "text", + }, + ], + }, + appInfo: { language: "en-US", platform: "Linux x86_64" }, + calculationOrder: [], + dispatchEventName: "_dispatchMe", + }; + sandbox.createSandbox(data); + + try { + await myeval(`(this.foobar = 123456, 0)`); + await myeval(`this.getField("field").doc.foobar`).then(value => { + expect(value).toEqual(123456); + }); + done(); + } catch (ex) { + done.fail(ex); + } + }); + }); + describe("Util", function () { beforeAll(function (done) { sandbox.createSandbox({