JS -- Actions must be evaluated in global scope

* All the public properties of doc are injected into globalThis, in order to make them available through `this`
 * Put event in the global scope too.
This commit is contained in:
Calixte Denizet 2020-12-16 14:04:22 +01:00
parent c366390f6b
commit 167ff1a7fc
7 changed files with 178 additions and 53 deletions

View File

@ -58,6 +58,9 @@ class Sandbox {
} }
create(data) { create(data) {
if (TESTING) {
this._module.ccall("nukeSandbox", null, []);
}
const sandboxData = JSON.stringify(data); const sandboxData = JSON.stringify(data);
const code = [ const code = [
// Next line is replaced by code from initialization.js // Next line is replaced by code from initialization.js

View File

@ -60,10 +60,7 @@ class AForm {
} }
} }
AFMergeChange(event) { AFMergeChange(event = globalThis.event) {
if (!event) {
event = this._document._event;
}
if (event.willCommit) { if (event.willCommit) {
return event.value.toString(); return event.value.toString();
} }
@ -128,7 +125,7 @@ class AForm {
strCurrency, strCurrency,
bCurrencyPrepend bCurrencyPrepend
) { ) {
const event = this._document._event; const event = globalThis.event;
if (!event.value) { if (!event.value) {
return; return;
} }
@ -193,7 +190,7 @@ class AForm {
strCurrency /* unused */, strCurrency /* unused */,
bCurrencyPrepend /* unused */ bCurrencyPrepend /* unused */
) { ) {
const event = this._document._event; const event = globalThis.event;
let value = this.AFMergeChange(event); let value = this.AFMergeChange(event);
if (!value) { if (!value) {
return; return;
@ -236,7 +233,7 @@ class AForm {
throw new Error("Invalid nDec value in AFPercent_Format"); throw new Error("Invalid nDec value in AFPercent_Format");
} }
const event = this._document._event; const event = globalThis.event;
if (nDec > 512) { if (nDec > 512) {
event.value = "%"; event.value = "%";
return; return;
@ -268,7 +265,7 @@ class AForm {
} }
AFDate_FormatEx(cFormat) { AFDate_FormatEx(cFormat) {
const event = this._document._event; const event = globalThis.event;
const value = event.value; const value = event.value;
if (!value) { if (!value) {
return; return;
@ -287,7 +284,7 @@ class AForm {
} }
AFDate_KeystrokeEx(cFormat) { AFDate_KeystrokeEx(cFormat) {
const event = this._document._event; const event = globalThis.event;
if (!event.willCommit) { if (!event.willCommit) {
return; return;
} }
@ -310,7 +307,7 @@ class AForm {
} }
AFRange_Validate(bGreaterThan, nGreaterThan, bLessThan, nLessThan) { AFRange_Validate(bGreaterThan, nGreaterThan, bLessThan, nLessThan) {
const event = this._document._event; const event = globalThis.event;
if (!event.value) { if (!event.value) {
return; return;
} }
@ -397,7 +394,7 @@ class AForm {
throw new TypeError("Invalid function in AFSimple_Calculate"); throw new TypeError("Invalid function in AFSimple_Calculate");
} }
const event = this._document._event; const event = globalThis.event;
const values = []; const values = [];
for (const cField of cFields) { for (const cField of cFields) {
const field = this._document.getField(cField); const field = this._document.getField(cField);
@ -417,7 +414,7 @@ class AForm {
} }
AFSpecial_Format(psf) { AFSpecial_Format(psf) {
const event = this._document._event; const event = globalThis.event;
if (!event.value) { if (!event.value) {
return; return;
} }
@ -457,7 +454,7 @@ class AForm {
return; return;
} }
const event = this._document._event; const event = globalThis.event;
const value = this.AFMergeChange(event); const value = this.AFMergeChange(event);
const checkers = new Map([ const checkers = new Map([
["9", char => char >= "0" && char <= "9"], ["9", char => char >= "0" && char <= "9"],
@ -526,7 +523,7 @@ class AForm {
} }
AFSpecial_Keystroke(psf) { AFSpecial_Keystroke(psf) {
const event = this._document._event; const event = globalThis.event;
if (!event.value) { if (!event.value) {
return; return;
} }

View File

@ -31,17 +31,21 @@ class Doc extends PDFObject {
constructor(data) { constructor(data) {
super(data); super(data);
this.baseURL = data.baseURL || ""; // In a script doc === this.
this.calculate = true; // So adding a property to the doc means adding it to this
this.delay = false; this._expandos = globalThis;
this.dirty = false;
this.disclosed = false; this._baseURL = data.baseURL || "";
this.media = undefined; this._calculate = true;
this.metadata = data.metadata; this._delay = false;
this.noautocomplete = undefined; this._dirty = false;
this.nocache = undefined; this._disclosed = false;
this.spellDictionaryOrder = []; this._media = undefined;
this.spellLanguageOrder = []; this._metadata = data.metadata;
this._noautocomplete = undefined;
this._nocache = undefined;
this._spellDictionaryOrder = [];
this._spellLanguageOrder = [];
this._printParams = null; this._printParams = null;
this._fields = new Map(); this._fields = new Map();
@ -127,6 +131,14 @@ class Doc extends PDFObject {
throw new Error("doc.author is read-only"); throw new Error("doc.author is read-only");
} }
get baseURL() {
return this._baseURL;
}
set baseURL(baseURL) {
this._baseURL = baseURL;
}
get bookmarkRoot() { get bookmarkRoot() {
return undefined; return undefined;
} }
@ -135,6 +147,14 @@ class Doc extends PDFObject {
throw new Error("doc.bookmarkRoot is read-only"); throw new Error("doc.bookmarkRoot is read-only");
} }
get calculate() {
return this._calculate;
}
set calculate(calculate) {
this._calculate = calculate;
}
get creator() { get creator() {
return this._creator; return this._creator;
} }
@ -151,6 +171,30 @@ class Doc extends PDFObject {
throw new Error("doc.dataObjects is read-only"); 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() { get docID() {
return this._docID; return this._docID;
} }
@ -278,6 +322,22 @@ class Doc extends PDFObject {
this._layout = value; 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() { get modDate() {
return this._modDate; return this._modDate;
} }
@ -302,6 +362,22 @@ class Doc extends PDFObject {
throw new Error("doc.mouseY is read-only"); 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() { get numFields() {
return this._numFields; return this._numFields;
} }
@ -418,6 +494,22 @@ class Doc extends PDFObject {
throw new Error("doc.sounds is read-only"); 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() { get subject() {
return this._subject; return this._subject;
} }

View File

@ -70,7 +70,7 @@ class EventDispatcher {
const name = baseEvent.name.replace(" ", ""); const name = baseEvent.name.replace(" ", "");
const source = this._objects[id]; const source = this._objects[id];
const event = (this._document.obj._event = new Event(baseEvent)); globalThis.event = new Event(baseEvent);
let savedChange; let savedChange;
if (source.obj._isButton()) { if (source.obj._isButton()) {
@ -155,7 +155,7 @@ class EventDispatcher {
} }
const first = this._calculationOrder[0]; const first = this._calculationOrder[0];
const source = this._objects[first]; const source = this._objects[first];
const event = (this._document.obj._event = new Event({})); globalThis.event = new Event({});
this.runCalculate(source, event); this.runCalculate(source, event);
} }

View File

@ -77,6 +77,8 @@ class Field extends PDFObject {
this._fillColor = data.fillColor || ["T"]; this._fillColor = data.fillColor || ["T"];
this._strokeColor = data.strokeColor || ["G", 0]; this._strokeColor = data.strokeColor || ["G", 0];
this._textColor = data.textColor || ["G", 0]; this._textColor = data.textColor || ["G", 0];
this._globalEval = data.globalEval;
} }
get fillColor() { get fillColor() {
@ -117,20 +119,6 @@ class Field extends PDFObject {
this._valueAsString = val ? val.toString() : ""; 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) { setAction(cTrigger, cScript) {
if (typeof cTrigger !== "string" || typeof cScript !== "string") { if (typeof cTrigger !== "string" || typeof cScript !== "string") {
return; return;
@ -138,10 +126,7 @@ class Field extends PDFObject {
if (!(cTrigger in this._actions)) { if (!(cTrigger in this._actions)) {
this._actions[cTrigger] = []; this._actions[cTrigger] = [];
} }
const fun = this._getFunction(cScript, cTrigger); this._actions[cTrigger].push(cScript);
if (fun) {
this._actions[cTrigger].push(fun);
}
} }
setFocus() { setFocus() {
@ -152,12 +137,7 @@ class Field extends PDFObject {
const actionsMap = new Map(); const actionsMap = new Map();
if (actions) { if (actions) {
for (const [eventType, actionsForEvent] of Object.entries(actions)) { for (const [eventType, actionsForEvent] of Object.entries(actions)) {
const functions = actionsForEvent actionsMap.set(eventType, actionsForEvent);
.map(action => this._getFunction(action, eventType))
.filter(fun => !!fun);
if (functions.length > 0) {
actionsMap.set(eventType, functions);
}
} }
} }
return actionsMap; return actionsMap;
@ -176,7 +156,8 @@ class Field extends PDFObject {
const actions = this._actions.get(eventName); const actions = this._actions.get(eventName);
try { try {
for (const action of actions) { for (const action of actions) {
action(event); // Action evaluation must happen in the global scope
this._globalEval(action);
} }
} catch (error) { } catch (error) {
event.rc = false; event.rc = false;

View File

@ -72,6 +72,7 @@ function initSandbox(params) {
obj.send = send; obj.send = send;
obj.globalEval = globalEval; obj.globalEval = globalEval;
obj.doc = _document.wrapped; obj.doc = _document.wrapped;
obj.globalEval = globalEval;
const field = new Field(obj); const field = new Field(obj);
const wrapped = new Proxy(field, proxyHandler); const wrapped = new Proxy(field, proxyHandler);
doc._addField(name, wrapped); doc._addField(name, wrapped);
@ -81,7 +82,6 @@ function initSandbox(params) {
globalThis.event = null; globalThis.event = null;
globalThis.global = Object.create(null); globalThis.global = Object.create(null);
globalThis.app = new Proxy(app, proxyHandler); globalThis.app = new Proxy(app, proxyHandler);
globalThis.doc = _document.wrapped;
globalThis.color = new Proxy(new Color(), proxyHandler); globalThis.color = new Proxy(new Color(), proxyHandler);
globalThis.console = new Proxy(new Console({ send }), proxyHandler); globalThis.console = new Proxy(new Console({ send }), proxyHandler);
globalThis.util = new Proxy(util, 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 = { const functions = {
dispatchEvent: app._dispatchEvent.bind(app), dispatchEvent: app._dispatchEvent.bind(app),
timeoutCb: app._evalCallback.bind(app), timeoutCb: app._evalCallback.bind(app),

View File

@ -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 () { describe("Util", function () {
beforeAll(function (done) { beforeAll(function (done) {
sandbox.createSandbox({ sandbox.createSandbox({