From 167ff1a7fcbdc96d4af4fa757f7ec372dd110152 Mon Sep 17 00:00:00 2001
From: Calixte Denizet <calixte.denizet@gmail.com>
Date: Wed, 16 Dec 2020 14:04:22 +0100
Subject: [PATCH] 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.

---
 src/pdf.sandbox.js                  |   3 +
 src/scripting_api/aform.js          |  25 +++---
 src/scripting_api/doc.js            | 114 +++++++++++++++++++++++++---
 src/scripting_api/event.js          |   4 +-
 src/scripting_api/field.js          |  31 ++------
 src/scripting_api/initialization.js |  22 +++++-
 test/unit/scripting_spec.js         |  32 ++++++++
 7 files changed, 178 insertions(+), 53 deletions(-)

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({