From d319c5bbdcc819f44c926381e5a0cf92494f2cca Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 10 Apr 2026 11:12:52 +0200 Subject: [PATCH] [JS] Hide implementation details in the scripting sandbox Replace the Proxy-based access control in the scripting API with real JavaScript private fields (#field, #method), eliminating the runtime overhead and complexity of Proxy traps. Introduce a `static claimInternals()` pattern for sharing internal capabilities between trusted initialization code and class instances. Each method is a one-shot: it deletes itself from the class object on first call and returns a capability object (bound private methods / accessors). Once `initSandbox` completes, no capability factory is reachable from the embedded PDF script scope. Each scripting module registers its class on `globalThis` as a side-effect of being imported. `initSandbox` then immediately captures and deletes every class from `globalThis` before any embedded PDF script runs, ensuring that class constructors and therefore private field access are unreachable from script scope. It should avoid any name conflicts. --- src/pdf.scripting.js | 6 +- src/scripting_api/aform.js | 107 ++-- src/scripting_api/app.js | 274 +++++----- src/scripting_api/app_utils.js | 30 +- src/scripting_api/color.js | 66 ++- src/scripting_api/common.js | 52 -- src/scripting_api/console.js | 16 +- src/scripting_api/constants.js | 221 ++++---- src/scripting_api/doc.js | 504 ++++++++++-------- src/scripting_api/error.js | 23 - src/scripting_api/event.js | 229 +++++---- src/scripting_api/field.js | 770 +++++++++++++++++++--------- src/scripting_api/fullscreen.js | 49 +- src/scripting_api/initialization.js | 409 ++++++++------- src/scripting_api/pdf_object.js | 24 - src/scripting_api/print_params.js | 6 +- src/scripting_api/proxy.js | 137 ----- src/scripting_api/thermometer.js | 30 +- src/scripting_api/util.js | 59 ++- src/shared/scripting_utils.js | 34 +- 20 files changed, 1630 insertions(+), 1416 deletions(-) delete mode 100644 src/scripting_api/common.js delete mode 100644 src/scripting_api/error.js delete mode 100644 src/scripting_api/pdf_object.js delete mode 100644 src/scripting_api/proxy.js diff --git a/src/pdf.scripting.js b/src/pdf.scripting.js index 4dc95b4520580..110a69458266c 100644 --- a/src/pdf.scripting.js +++ b/src/pdf.scripting.js @@ -13,8 +13,4 @@ * limitations under the License. */ -import { initSandbox } from "./scripting_api/initialization.js"; - -// To avoid problems with `export` statements in the QuickJS Javascript Engine, -// we manually expose `pdfjsScripting` globally instead. -globalThis.pdfjsScripting = { initSandbox }; +import "./scripting_api/initialization.js"; diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js index d390044457988..809fedb9f60b2 100644 --- a/src/scripting_api/aform.js +++ b/src/scripting_api/aform.js @@ -14,33 +14,53 @@ */ import { DateFormats, TimeFormats } from "../shared/scripting_utils.js"; -import { GlobalConstants } from "./constants.js"; +import { + IDS_GREATER_THAN, + IDS_GT_AND_LT, + IDS_INVALID_DATE, + IDS_INVALID_DATE2, + IDS_INVALID_VALUE, + IDS_LESS_THAN, +} from "./constants.js"; import { MathClamp } from "../shared/math_clamp.js"; -class AForm { - constructor(document, app, util, color) { - this._document = document; - this._app = app; - this._util = util; - this._color = color; +globalThis.AForm = class AForm { + #document; + + #app; + + #util; + + #utilInternals; + + #mergeChange; + + #emailRegex; + + constructor(document, app, util, utilInternals, mergeChange) { + this.#document = document; + this.#app = app; + this.#util = util; + this.#utilInternals = utilInternals; + this.#mergeChange = mergeChange; // The e-mail address regex below originates from: // https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address - this._emailRegex = new RegExp( + this.#emailRegex = new RegExp( "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+" + "@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" + "(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ); } - _mkTargetName(event) { + #mkTargetName(event) { return event.target ? `[ ${event.target.name} ]` : ""; } - _parseDate(cFormat, cDate) { + #parseDate(cFormat, cDate) { let date = null; try { - date = this._util._scand(cFormat, cDate, /* strict = */ false); + date = this.#utilInternals.scand(cFormat, cDate, /* strict = */ false); } catch {} if (date) { return date; @@ -55,11 +75,11 @@ class AForm { return event.value.toString(); } - return this._app._eventDispatcher.mergeChange(event); + return this.#mergeChange(event); } AFParseDateEx(cString, cOrder) { - return this._parseDate(cOrder, cString); + return this.#parseDate(cOrder, cString); } AFExtractNums(str) { @@ -153,7 +173,8 @@ class AForm { } if (negStyle === 1 || negStyle === 3) { - event.target.textColor = sign === 1 ? this._color.black : this._color.red; + event.target.textColor = + sign === 1 ? globalThis.color.black : globalThis.color.red; } if ((negStyle !== 0 || bCurrencyPrepend) && sign === -1) { @@ -161,7 +182,7 @@ class AForm { } const formatStr = buf.join(""); - event.value = this._util.printf(formatStr, value); + event.value = this.#util.printf(formatStr, value); } AFNumber_Keystroke( @@ -194,10 +215,8 @@ class AForm { if (!pattern.test(value)) { if (event.willCommit) { - const err = `${GlobalConstants.IDS_INVALID_VALUE} ${this._mkTargetName( - event - )}`; - this._app.alert(err); + const err = `${IDS_INVALID_VALUE} ${this.#mkTargetName(event)}`; + this.#app.alert(err); } event.rc = false; } @@ -236,7 +255,7 @@ class AForm { } const formatStr = `%,${sepStyle}.${nDec}f`; - value = this._util.printf(formatStr, value * 100); + value = this.#util.printf(formatStr, value * 100); event.value = percentPrepend ? `%${value}` : `${value}%`; } @@ -252,9 +271,9 @@ class AForm { return; } - const date = this._parseDate(cFormat, value); + const date = this.#parseDate(cFormat, value); if (date !== null) { - event.value = this._util.printd(cFormat, date); + event.value = this.#util.printd(cFormat, date); } } @@ -273,13 +292,13 @@ class AForm { return; } - if (this._parseDate(cFormat, value) === null) { - const invalid = GlobalConstants.IDS_INVALID_DATE; - const invalid2 = GlobalConstants.IDS_INVALID_DATE2; - const err = `${invalid} ${this._mkTargetName( + if (this.#parseDate(cFormat, value) === null) { + const invalid = IDS_INVALID_DATE; + const invalid2 = IDS_INVALID_DATE2; + const err = `${invalid} ${this.#mkTargetName( event )}${invalid2}${cFormat}`; - this._app.alert(err); + this.#app.alert(err); event.rc = false; } } @@ -321,21 +340,17 @@ class AForm { let err = ""; if (bGreaterThan && bLessThan) { if (value < nGreaterThan || value > nLessThan) { - err = this._util.printf( - GlobalConstants.IDS_GT_AND_LT, - nGreaterThan, - nLessThan - ); + err = this.#util.printf(IDS_GT_AND_LT, nGreaterThan, nLessThan); } } else if (bGreaterThan) { if (value < nGreaterThan) { - err = this._util.printf(GlobalConstants.IDS_GREATER_THAN, nGreaterThan); + err = this.#util.printf(IDS_GREATER_THAN, nGreaterThan); } } else if (value > nLessThan) { - err = this._util.printf(GlobalConstants.IDS_LESS_THAN, nLessThan); + err = this.#util.printf(IDS_LESS_THAN, nLessThan); } if (err) { - this._app.alert(err); + this.#app.alert(err); event.rc = false; } } @@ -385,7 +400,7 @@ class AForm { cFields = this.AFMakeArrayFromList(cFields); for (const cField of cFields) { - const field = this._document.getField(cField); + const field = this.#document.getField(cField); if (!field) { continue; } @@ -422,7 +437,7 @@ class AForm { break; case 2: formatStr = - this._util.printx("9999999999", event.value).length >= 10 + this.#util.printx("9999999999", event.value).length >= 10 ? "(999) 999-9999" : "999-9999"; break; @@ -433,7 +448,7 @@ class AForm { throw new Error("Invalid psf in AFSpecial_Format"); } - event.value = this._util.printx(formatStr, event.value); + event.value = this.#util.printx(formatStr, event.value); } AFSpecial_KeystrokeEx(cMask) { @@ -495,11 +510,11 @@ class AForm { return true; } - const err = `${GlobalConstants.IDS_INVALID_VALUE} = "${cMask}"`; + const err = `${IDS_INVALID_VALUE} = "${cMask}"`; if (value.length > cMask.length) { if (warn) { - this._app.alert(err); + this.#app.alert(err); } event.rc = false; return; @@ -508,7 +523,7 @@ class AForm { if (event.willCommit) { if (value.length < cMask.length) { if (warn) { - this._app.alert(err); + this.#app.alert(err); } event.rc = false; return; @@ -516,7 +531,7 @@ class AForm { if (!_checkValidity(value, cMask)) { if (warn) { - this._app.alert(err); + this.#app.alert(err); } event.rc = false; return; @@ -531,7 +546,7 @@ class AForm { if (!_checkValidity(value, cMask)) { if (warn) { - this._app.alert(err); + this.#app.alert(err); } event.rc = false; } @@ -611,7 +626,7 @@ class AForm { } eMailValidate(str) { - return this._emailRegex.test(str); + return this.#emailRegex.test(str); } AFExactMatch(rePatterns, str) { @@ -621,6 +636,6 @@ class AForm { return rePatterns.findIndex(re => str.match(re)?.[0] === str) + 1; } -} +}; -export { AForm }; +export {}; diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index 1680686bc5759..14d9c7d8b7514 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -13,129 +13,149 @@ * limitations under the License. */ -import { - FORMS_VERSION, - USERACTIVATION_CALLBACKID, - VIEWER_TYPE, - VIEWER_VARIATION, - VIEWER_VERSION, -} from "./app_utils.js"; -import { Color } from "./color.js"; -import { EventDispatcher } from "./event.js"; -import { FullScreen } from "./fullscreen.js"; -import { PDFObject } from "./pdf_object.js"; -import { Thermometer } from "./thermometer.js"; - -class App extends PDFObject { +import "./fullscreen.js"; +import "./thermometer.js"; + +globalThis.App = class App { + static claimInternals() { + delete App.claimInternals; + return instance => ({ + evalCallback: instance.#evalCallback.bind(instance), + }); + } + + #constants = null; + + #focusRect = true; + + #fs = null; + + #language; + + #openInPlace = false; + + #platform; + + #runtimeHighlight = false; + + #runtimeHighlightColor = ["T"]; + + #thermometer = null; + + #toolbar = false; + + #doc; + + #timeoutIds; + + #timeoutIdsRegistry; + + #timeoutCallbackIds; + + #timeoutCallbackId; + + #globalEval; + + #externalCall; + + #send; + + #userActivationData; + constructor(data) { - super(data); - - this._constants = null; - this._focusRect = true; - this._fs = null; - this._language = App._getLanguage(data.language); - this._openInPlace = false; - this._platform = App._getPlatform(data.platform); - this._runtimeHighlight = false; - this._runtimeHighlightColor = ["T"]; - this._thermometer = null; - this._toolbar = false; - - this._document = data._document; - this._proxyHandler = data.proxyHandler; - this._objects = Object.create(null); - this._eventDispatcher = new EventDispatcher( - this._document, - data.calculationOrder, - this._objects, - data.externalCall - ); + this.#send = data.send; + this.#userActivationData = data.userActivationData; - this._timeoutIds = new WeakMap(); + this.#language = App.#getLanguage(data.language); + this.#platform = App.#getPlatform(data.platform); + + this.#doc = data.doc; + + this.#timeoutIds = new WeakMap(); // About setTimeOut/setInterval return values (specs): // The return value of this method must be held in a // JavaScript variable. // Otherwise, the timeout object is subject to garbage-collection, // which would cause the clock to stop. - this._timeoutIdsRegistry = new FinalizationRegistry( - this._cleanTimeout.bind(this) + this.#timeoutIdsRegistry = new FinalizationRegistry( + this.#cleanTimeout.bind(this) ); - this._timeoutCallbackIds = new Map(); - this._timeoutCallbackId = USERACTIVATION_CALLBACKID + 1; - this._globalEval = data.globalEval; - this._externalCall = data.externalCall; - } + this.#timeoutCallbackIds = new Map(); + // User activation callback id is 0. + this.#timeoutCallbackId = 1; + this.#globalEval = data.globalEval; + this.#externalCall = data.externalCall; - // This function is called thanks to the proxy - // when we call app['random_string'] to dispatch the event. - _dispatchEvent(pdfEvent) { - this._eventDispatcher.dispatch(pdfEvent); + this.#fs = new globalThis.FullScreen(); + delete globalThis.FullScreen; + this.#thermometer = new globalThis.Thermometer(); + delete globalThis.Thermometer; } - _registerTimeoutCallback(cExpr) { - const id = this._timeoutCallbackId++; - this._timeoutCallbackIds.set(id, cExpr); + #registerTimeoutCallback(cExpr) { + const id = this.#timeoutCallbackId++; + this.#timeoutCallbackIds.set(id, cExpr); return id; } - _unregisterTimeoutCallback(id) { - this._timeoutCallbackIds.delete(id); + #unregisterTimeoutCallback(id) { + this.#timeoutCallbackIds.delete(id); } - _evalCallback({ callbackId, interval }) { - const documentObj = this._document.obj; + #evalCallback({ callbackId, interval }) { + const USERACTIVATION_CALLBACKID = 0; if (callbackId === USERACTIVATION_CALLBACKID) { // Special callback id for userActivation stuff. - documentObj._userActivation = false; + this.#userActivationData.userActivation = false; return; } - const expr = this._timeoutCallbackIds.get(callbackId); + const expr = this.#timeoutCallbackIds.get(callbackId); if (!interval) { - this._unregisterTimeoutCallback(callbackId); + this.#unregisterTimeoutCallback(callbackId); } if (expr) { - const saveUserActivation = documentObj._userActivation; + const saveUserActivation = this.#userActivationData.userActivation; // A setTimeout/setInterval callback is executed so it can't be a user // choice. - documentObj._userActivation = false; - this._globalEval(expr); - documentObj._userActivation = saveUserActivation; + this.#userActivationData.userActivation = false; + this.#globalEval(expr); + this.#userActivationData.userActivation = saveUserActivation; } } - _registerTimeout(callbackId, interval) { + #registerTimeout(callbackId, interval) { const timeout = Object.create(null); const id = { callbackId, interval }; - this._timeoutIds.set(timeout, id); - this._timeoutIdsRegistry.register(timeout, id); + this.#timeoutIds.set(timeout, id); + this.#timeoutIdsRegistry.register(timeout, id); return timeout; } - _unregisterTimeout(timeout) { - this._timeoutIdsRegistry.unregister(timeout); + #unregisterTimeout(timeout) { + this.#timeoutIdsRegistry.unregister(timeout); - const data = this._timeoutIds.get(timeout); + const data = this.#timeoutIds.get(timeout); if (!data) { return; } - this._timeoutIds.delete(timeout); - this._cleanTimeout(data); + this.#timeoutIds.delete(timeout); + this.#cleanTimeout(data); } - _cleanTimeout({ callbackId, interval }) { - this._unregisterTimeoutCallback(callbackId); + #cleanTimeout({ callbackId, interval }) { + this.#unregisterTimeoutCallback(callbackId); if (interval) { - this._externalCall("clearInterval", [callbackId]); + this.#externalCall("clearInterval", [callbackId]); } else { - this._externalCall("clearTimeout", [callbackId]); + this.#externalCall("clearTimeout", [callbackId]); } } - static _getPlatform(platform) { + static #getPlatform(platform) { if (typeof platform === "string") { platform = platform.toLowerCase(); if (platform.includes("win")) { @@ -147,7 +167,7 @@ class App extends PDFObject { return "UNIX"; } - static _getLanguage(language) { + static #getLanguage(language) { const [main, sub] = language.toLowerCase().split(/[-_]/, 2); switch (main) { case "zh": @@ -182,7 +202,7 @@ class App extends PDFObject { } get activeDocs() { - return [this._document.wrapped]; + return [globalThis]; } set activeDocs(_) { @@ -190,15 +210,15 @@ class App extends PDFObject { } get calculate() { - return this._document.obj.calculate; + return this.#doc.calculate; } set calculate(calculate) { - this._document.obj.calculate = calculate; + this.#doc.calculate = calculate; } get constants() { - return (this._constants ??= Object.freeze({ + return (this.#constants ??= Object.freeze({ align: Object.freeze({ left: 0, center: 1, @@ -214,16 +234,16 @@ class App extends PDFObject { } get focusRect() { - return this._focusRect; + return this.#focusRect; } set focusRect(val) { /* TODO or not */ - this._focusRect = val; + this.#focusRect = val; } get formsVersion() { - return FORMS_VERSION; + return 21.00720099; } set formsVersion(_) { @@ -239,10 +259,7 @@ class App extends PDFObject { } get fs() { - return (this._fs ??= new Proxy( - new FullScreen({ send: this._send }), - this._proxyHandler - )); + return this.#fs; } set fs(_) { @@ -250,7 +267,7 @@ class App extends PDFObject { } get language() { - return this._language; + return this.#language; } set language(_) { @@ -282,16 +299,16 @@ class App extends PDFObject { } get openInPlace() { - return this._openInPlace; + return this.#openInPlace; } set openInPlace(val) { - this._openInPlace = val; + this.#openInPlace = val; /* TODO */ } get platform() { - return this._platform; + return this.#platform; } set platform(_) { @@ -323,30 +340,27 @@ class App extends PDFObject { } get runtimeHighlight() { - return this._runtimeHighlight; + return this.#runtimeHighlight; } set runtimeHighlight(val) { - this._runtimeHighlight = val; + this.#runtimeHighlight = val; /* TODO */ } get runtimeHighlightColor() { - return this._runtimeHighlightColor; + return this.#runtimeHighlightColor; } set runtimeHighlightColor(val) { - if (Color._isValidColor(val)) { - this._runtimeHighlightColor = val; + if (globalThis.color._isValidColor(val)) { + this.#runtimeHighlightColor = val; /* TODO */ } } get thermometer() { - return (this._thermometer ??= new Proxy( - new Thermometer({ send: this._send }), - this._proxyHandler - )); + return this.#thermometer; } set thermometer(_) { @@ -354,11 +368,11 @@ class App extends PDFObject { } get toolbar() { - return this._toolbar; + return this.#toolbar; } set toolbar(val) { - this._toolbar = val; + this.#toolbar = val; /* TODO */ } @@ -381,7 +395,7 @@ class App extends PDFObject { } get viewerType() { - return VIEWER_TYPE; + return "PDF.js"; } set viewerType(_) { @@ -389,7 +403,7 @@ class App extends PDFObject { } get viewerVariation() { - return VIEWER_VARIATION; + return "Full"; } set viewerVariation(_) { @@ -397,7 +411,7 @@ class App extends PDFObject { } get viewerVersion() { - return VIEWER_VERSION; + return 21.00720099; } set viewerVersion(_) { @@ -424,10 +438,10 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { - if (!this._document.obj._userActivation) { + if (!this.#userActivationData.userActivation) { return 0; } - this._document.obj._userActivation = false; + this.#userActivationData.userActivation = false; if (cMsg && typeof cMsg === "object") { nType = cMsg.nType; @@ -442,10 +456,10 @@ class App extends PDFObject { ? 0 : nType; if (nType >= 2) { - return this._externalCall("confirm", [cMsg]) ? 4 : 3; + return this.#externalCall("confirm", [cMsg]) ? 4 : 3; } - this._externalCall("alert", [cMsg]); + this.#externalCall("alert", [cMsg]); return 1; } @@ -462,11 +476,11 @@ class App extends PDFObject { } clearInterval(oInterval) { - this._unregisterTimeout(oInterval); + this.#unregisterTimeout(oInterval); } clearTimeOut(oTime) { - this._unregisterTimeout(oTime); + this.#unregisterTimeout(oTime); } endPriv() { @@ -478,17 +492,17 @@ class App extends PDFObject { } execMenuItem(item) { - if (!this._document.obj._userActivation) { + if (!this.#userActivationData.userActivation) { return; } - this._document.obj._userActivation = false; + this.#userActivationData.userActivation = false; switch (item) { case "SaveAs": - if (this._document.obj._disableSaving) { + if (this.#userActivationData.disableSaving) { return; } - this._send({ command: item }); + this.#send({ command: item }); break; case "FirstPage": case "LastPage": @@ -496,16 +510,16 @@ class App extends PDFObject { case "PrevPage": case "ZoomViewIn": case "ZoomViewOut": - this._send({ command: item }); + this.#send({ command: item }); break; case "FitPage": - this._send({ command: "zoom", value: "page-fit" }); + this.#send({ command: "zoom", value: "page-fit" }); break; case "Print": - if (this._document.obj._disablePrinting) { + if (this.#userActivationData.disablePrinting) { return; } - this._send({ command: "print" }); + this.#send({ command: "print" }); break; } } @@ -591,10 +605,10 @@ class App extends PDFObject { } response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { - if (!this._document.obj._userActivation) { + if (!this.#userActivationData.userActivation) { return null; } - this._document.obj._userActivation = false; + this.#userActivationData.userActivation = false; if (cQuestion && typeof cQuestion === "object") { cDefault = cQuestion.cDefault; @@ -602,7 +616,7 @@ class App extends PDFObject { } cQuestion = (cQuestion || "").toString(); cDefault = (cDefault || "").toString(); - return this._externalCall("prompt", [cQuestion, cDefault || ""]); + return this.#externalCall("prompt", [cQuestion, cDefault || ""]); } setInterval(cExpr, nMilliseconds = 0) { @@ -619,9 +633,9 @@ class App extends PDFObject { "Second argument of app.setInterval must be a number" ); } - const callbackId = this._registerTimeoutCallback(cExpr); - this._externalCall("setInterval", [callbackId, nMilliseconds]); - return this._registerTimeout(callbackId, true); + const callbackId = this.#registerTimeoutCallback(cExpr); + this.#externalCall("setInterval", [callbackId, nMilliseconds]); + return this.#registerTimeout(callbackId, true); } setTimeOut(cExpr, nMilliseconds = 0) { @@ -636,9 +650,9 @@ class App extends PDFObject { if (typeof nMilliseconds !== "number") { throw new TypeError("Second argument of app.setTimeOut must be a number"); } - const callbackId = this._registerTimeoutCallback(cExpr); - this._externalCall("setTimeout", [callbackId, nMilliseconds]); - return this._registerTimeout(callbackId, false); + const callbackId = this.#registerTimeoutCallback(cExpr); + this.#externalCall("setTimeout", [callbackId, nMilliseconds]); + return this.#registerTimeout(callbackId, false); } trustedFunction() { @@ -648,6 +662,6 @@ class App extends PDFObject { trustPropagatorFunction() { /* Not implemented */ } -} +}; -export { App }; +export {}; diff --git a/src/scripting_api/app_utils.js b/src/scripting_api/app_utils.js index e4378ebf7b0de..a91eebe451c7f 100644 --- a/src/scripting_api/app_utils.js +++ b/src/scripting_api/app_utils.js @@ -13,24 +13,6 @@ * limitations under the License. */ -const VIEWER_TYPE = "PDF.js"; -const VIEWER_VARIATION = "Full"; -const VIEWER_VERSION = 21.00720099; -const FORMS_VERSION = 21.00720099; - -const USERACTIVATION_CALLBACKID = 0; -const USERACTIVATION_MAXTIME_VALIDITY = 5000; - -function serializeError(error) { - const value = `${error.toString()}\n${error.stack}`; - return { command: "error", value }; -} - -// Helpers for simple `Map.prototype.getOrInsertComputed()` invocations, -// to avoid duplicate function creation. -const makeArr = () => []; -const makeMap = () => new Map(); - if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { // TODO: Remove this once `Math.sumPrecise` is supported in QuickJS. // @@ -55,14 +37,4 @@ if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { } } -export { - FORMS_VERSION, - makeArr, - makeMap, - serializeError, - USERACTIVATION_CALLBACKID, - USERACTIVATION_MAXTIME_VALIDITY, - VIEWER_TYPE, - VIEWER_VARIATION, - VIEWER_VERSION, -}; +export {}; diff --git a/src/scripting_api/color.js b/src/scripting_api/color.js index dee798d5e9e6e..da2ffff8a3eb0 100644 --- a/src/scripting_api/color.js +++ b/src/scripting_api/color.js @@ -14,38 +14,33 @@ */ import { ColorConverters } from "../shared/scripting_utils.js"; -import { PDFObject } from "./pdf_object.js"; -class Color extends PDFObject { - transparent = ["T"]; +globalThis.color = Object.freeze({ + transparent: ["T"], - black = ["G", 0]; + black: ["G", 0], - white = ["G", 1]; + white: ["G", 1], - red = ["RGB", 1, 0, 0]; + red: ["RGB", 1, 0, 0], - green = ["RGB", 0, 1, 0]; + green: ["RGB", 0, 1, 0], - blue = ["RGB", 0, 0, 1]; + blue: ["RGB", 0, 0, 1], - cyan = ["CMYK", 1, 0, 0, 0]; + cyan: ["CMYK", 1, 0, 0, 0], - magenta = ["CMYK", 0, 1, 0, 0]; + magenta: ["CMYK", 0, 1, 0, 0], - yellow = ["CMYK", 0, 0, 1, 0]; + yellow: ["CMYK", 0, 0, 1, 0], - dkGray = ["G", 0.25]; + dkGray: ["G", 0.25], - gray = ["G", 0.5]; + gray: ["G", 0.5], - ltGray = ["G", 0.75]; + ltGray: ["G", 0.75], - constructor() { - super({}); - } - - static _isValidSpace(cColorSpace) { + _isValidSpace(cColorSpace) { return ( typeof cColorSpace === "string" && (cColorSpace === "T" || @@ -53,14 +48,14 @@ class Color extends PDFObject { cColorSpace === "RGB" || cColorSpace === "CMYK") ); - } + }, - static _isValidColor(colorArray) { + _isValidColor(colorArray) { if (!Array.isArray(colorArray) || colorArray.length === 0) { return false; } const space = colorArray[0]; - if (!Color._isValidSpace(space)) { + if (!this._isValidSpace(space)) { return false; } @@ -92,14 +87,14 @@ class Color extends PDFObject { return colorArray .slice(1) .every(c => typeof c === "number" && c >= 0 && c <= 1); - } + }, - static _getCorrectColor(colorArray) { - return Color._isValidColor(colorArray) ? colorArray : ["G", 0]; - } + _getCorrectColor(colorArray) { + return this._isValidColor(colorArray) ? colorArray : ["G", 0]; + }, convert(colorArray, cColorSpace) { - if (!Color._isValidSpace(cColorSpace)) { + if (!this._isValidSpace(cColorSpace)) { return this.black; } @@ -107,7 +102,7 @@ class Color extends PDFObject { return ["T"]; } - colorArray = Color._getCorrectColor(colorArray); + colorArray = this._getCorrectColor(colorArray); if (colorArray[0] === cColorSpace) { return colorArray; } @@ -119,11 +114,11 @@ class Color extends PDFObject { return ColorConverters[`${colorArray[0]}_${cColorSpace}`]( colorArray.slice(1) ); - } + }, equal(colorArray1, colorArray2) { - colorArray1 = Color._getCorrectColor(colorArray1); - colorArray2 = Color._getCorrectColor(colorArray2); + colorArray1 = this._getCorrectColor(colorArray1); + colorArray2 = this._getCorrectColor(colorArray2); if (colorArray1[0] === "T" || colorArray2[0] === "T") { return colorArray1[0] === "T" && colorArray2[0] === "T"; @@ -134,7 +129,10 @@ class Color extends PDFObject { } return colorArray1.slice(1).every((c, i) => c === colorArray2[i + 1]); - } -} + }, +}); + +globalThis.ColorConvert = globalThis.color.convert.bind(globalThis.color); +globalThis.ColorEqual = globalThis.color.equal.bind(globalThis.color); -export { Color }; +export {}; diff --git a/src/scripting_api/common.js b/src/scripting_api/common.js deleted file mode 100644 index aa1b4c0544769..0000000000000 --- a/src/scripting_api/common.js +++ /dev/null @@ -1,52 +0,0 @@ -/* Copyright 2020 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const FieldType = { - none: 0, - number: 1, - percent: 2, - date: 3, - time: 4, -}; - -function createActionsMap(actions) { - return new Map(actions ? Object.entries(actions) : null); -} - -function getFieldType(actions) { - let format = actions.get("Format"); - if (!format) { - return FieldType.none; - } - - format = format[0]; - - format = format.trim(); - if (format.startsWith("AFNumber_")) { - return FieldType.number; - } - if (format.startsWith("AFPercent_")) { - return FieldType.percent; - } - if (format.startsWith("AFDate_")) { - return FieldType.date; - } - if (format.startsWith("AFTime_")) { - return FieldType.time; - } - return FieldType.none; -} - -export { createActionsMap, FieldType, getFieldType }; diff --git a/src/scripting_api/console.js b/src/scripting_api/console.js index 9ed15577bfd58..d835b60eac86f 100644 --- a/src/scripting_api/console.js +++ b/src/scripting_api/console.js @@ -13,11 +13,15 @@ * limitations under the License. */ -import { PDFObject } from "./pdf_object.js"; +globalThis.Console = class Console { + #send; + + constructor({ send }) { + this.#send = send; + } -class Console extends PDFObject { clear() { - this._send({ id: "clear" }); + this.#send({ id: "clear" }); } hide() { @@ -32,12 +36,12 @@ class Console extends PDFObject { msg = msg.toString?.() || "[Unserializable object]"; } } - this._send({ command: "println", value: "PDF.js Console:: " + msg }); + this.#send({ command: "println", value: `PDF.js Console:: ${msg}` }); } show() { /* Not implemented */ } -} +}; -export { Console }; +export {}; diff --git a/src/scripting_api/constants.js b/src/scripting_api/constants.js index 43c0d76b028aa..a7dadf6078940 100644 --- a/src/scripting_api/constants.js +++ b/src/scripting_api/constants.js @@ -13,28 +13,25 @@ * limitations under the License. */ -const Border = Object.freeze({ +const border = Object.freeze({ s: "solid", d: "dashed", b: "beveled", i: "inset", u: "underline", }); - -const Cursor = Object.freeze({ +const cursor = Object.freeze({ visible: 0, hidden: 1, delay: 2, }); - -const Display = Object.freeze({ +const display = Object.freeze({ visible: 0, hidden: 1, noPrint: 2, noView: 3, }); - -const Font = Object.freeze({ +const font = Object.freeze({ Times: "Times-Roman", TimesB: "Times-Bold", TimesI: "Times-Italic", @@ -52,15 +49,13 @@ const Font = Object.freeze({ KaGo: "HeiseiKakuGo-W5-UniJIS-UCS2-H", KaMi: "HeiseiMin-W3-UniJIS-UCS2-H", }); - -const Highlight = Object.freeze({ +const highlight = Object.freeze({ n: "none", i: "invert", p: "push", o: "outline", }); - -const Position = Object.freeze({ +const position = Object.freeze({ textOnly: 0, iconOnly: 1, iconTextV: 2, @@ -69,20 +64,17 @@ const Position = Object.freeze({ textIconH: 5, overlay: 6, }); - -const ScaleHow = Object.freeze({ +const scaleHow = Object.freeze({ proportional: 0, anamorphic: 1, }); - -const ScaleWhen = Object.freeze({ +const scaleWhen = Object.freeze({ always: 0, never: 1, tooBig: 2, tooSmall: 3, }); - -const Style = Object.freeze({ +const style = Object.freeze({ ch: "check", cr: "cross", di: "diamond", @@ -90,8 +82,7 @@ const Style = Object.freeze({ st: "star", sq: "square", }); - -const Trans = Object.freeze({ +const trans = Object.freeze({ blindsH: "BlindsHorizontal", blindsV: "BlindsVertical", boxI: "BoxIn", @@ -111,8 +102,7 @@ const Trans = Object.freeze({ wipeR: "WipeRight", wipeU: "WipeUp", }); - -const ZoomType = Object.freeze({ +const zoomtype = Object.freeze({ none: "NoVary", fitP: "FitPage", fitW: "FitWidth", @@ -121,88 +111,113 @@ const ZoomType = Object.freeze({ pref: "Preferred", refW: "ReflowWidth", }); - -const GlobalConstants = Object.freeze({ - IDS_GREATER_THAN: "Invalid value: must be greater than or equal to % s.", - IDS_GT_AND_LT: - "Invalid value: must be greater than or equal to % s " + - "and less than or equal to % s.", - IDS_LESS_THAN: "Invalid value: must be less than or equal to % s.", - IDS_INVALID_MONTH: "** Invalid **", - IDS_INVALID_DATE: - "Invalid date / time: please ensure that the date / time exists. Field", - IDS_INVALID_DATE2: " should match format ", - IDS_INVALID_VALUE: "The value entered does not match the format of the field", - IDS_AM: "am", - IDS_PM: "pm", - IDS_MONTH_INFO: - "January[1] February[2] March[3] April[4] May[5] " + - "June[6] July[7] August[8] September[9] October[10] " + - "November[11] December[12] Sept[9] Jan[1] Feb[2] Mar[3] " + - "Apr[4] Jun[6] Jul[7] Aug[8] Sep[9] Oct[10] Nov[11] Dec[12]", - IDS_STARTUP_CONSOLE_MSG: "** ^ _ ^ **", - RE_NUMBER_ENTRY_DOT_SEP: ["[+-]?\\d*\\.?\\d*"], - RE_NUMBER_COMMIT_DOT_SEP: [ - // -1.0 or -1 - "[+-]?\\d+(\\.\\d+)?", - // -.1 - "[+-]?\\.\\d+", - // -1. - "[+-]?\\d+\\.", - ], - RE_NUMBER_ENTRY_COMMA_SEP: ["[+-]?\\d*,?\\d*"], - RE_NUMBER_COMMIT_COMMA_SEP: [ - // -1,0 or -1 - "[+-]?\\d+([.,]\\d+)?", - // -,1 - "[+-]?[.,]\\d+", - // -1, - "[+-]?\\d+[.,]", - ], - RE_ZIP_ENTRY: ["\\d{0,5}"], - RE_ZIP_COMMIT: ["\\d{5}"], - RE_ZIP4_ENTRY: ["\\d{0,5}(\\.|[- ])?\\d{0,4}"], - RE_ZIP4_COMMIT: ["\\d{5}(\\.|[- ])?\\d{4}"], - RE_PHONE_ENTRY: [ - // 555-1234 or 408 555-1234 - "\\d{0,3}(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", - // (408 - "\\(\\d{0,3}", - // (408) 555-1234 - // (allow the addition of parens as an afterthought) - "\\(\\d{0,3}\\)(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", - // (408 555-1234 - "\\(\\d{0,3}(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", - // 408) 555-1234 - "\\d{0,3}\\)(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", - // international - "011(\\.|[- \\d])*", - ], - RE_PHONE_COMMIT: [ - // 555-1234 - "\\d{3}(\\.|[- ])?\\d{4}", - // 408 555-1234 - "\\d{3}(\\.|[- ])?\\d{3}(\\.|[- ])?\\d{4}", - // (408) 555-1234 - "\\(\\d{3}\\)(\\.|[- ])?\\d{3}(\\.|[- ])?\\d{4}", - // international - "011(\\.|[- \\d])*", - ], - RE_SSN_ENTRY: ["\\d{0,3}(\\.|[- ])?\\d{0,2}(\\.|[- ])?\\d{0,4}"], - RE_SSN_COMMIT: ["\\d{3}(\\.|[- ])?\\d{2}(\\.|[- ])?\\d{4}"], -}); +const IDS_GREATER_THAN = "Invalid value: must be greater than or equal to % s."; +const IDS_GT_AND_LT = + "Invalid value: must be greater than or equal to % s " + + "and less than or equal to % s."; +const IDS_LESS_THAN = "Invalid value: must be less than or equal to % s."; +const IDS_INVALID_MONTH = "** Invalid **"; +const IDS_INVALID_DATE = + "Invalid date / time: please ensure that the date / time exists. Field"; +const IDS_INVALID_DATE2 = " should match format "; +const IDS_INVALID_VALUE = + "The value entered does not match the format of the field"; +const IDS_AM = "am"; +const IDS_PM = "pm"; +const IDS_MONTH_INFO = + "January[1] February[2] March[3] April[4] May[5] " + + "June[6] July[7] August[8] September[9] October[10] " + + "November[11] December[12] Sept[9] Jan[1] Feb[2] Mar[3] " + + "Apr[4] Jun[6] Jul[7] Aug[8] Sep[9] Oct[10] Nov[11] Dec[12]"; +const IDS_STARTUP_CONSOLE_MSG = "** ^ _ ^ **"; +const RE_NUMBER_ENTRY_DOT_SEP = ["[+-]?\\d*\\.?\\d*"]; +const RE_NUMBER_COMMIT_DOT_SEP = [ + // -1.0 or -1 + "[+-]?\\d+(\\.\\d+)?", + // -.1 + "[+-]?\\.\\d+", + // -1. + "[+-]?\\d+\\.", +]; +const RE_NUMBER_ENTRY_COMMA_SEP = ["[+-]?\\d*,?\\d*"]; +const RE_NUMBER_COMMIT_COMMA_SEP = [ + // -1,0 or -1 + "[+-]?\\d+([.,]\\d+)?", + // -,1 + "[+-]?[.,]\\d+", + // -1, + "[+-]?\\d+[.,]", +]; +const RE_ZIP_ENTRY = ["\\d{0,5}"]; +const RE_ZIP_COMMIT = ["\\d{5}"]; +const RE_ZIP4_ENTRY = ["\\d{0,5}(\\.|[- ])?\\d{0,4}"]; +const RE_ZIP4_COMMIT = ["\\d{5}(\\.|[- ])?\\d{4}"]; +const RE_PHONE_ENTRY = [ + // 555-1234 or 408 555-1234 + "\\d{0,3}(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", + // (408 + "\\(\\d{0,3}", + // (408) 555-1234 + // (allow the addition of parens as an afterthought) + "\\(\\d{0,3}\\)(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", + // (408 555-1234 + "\\(\\d{0,3}(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", + // 408) 555-1234 + "\\d{0,3}\\)(\\.|[- ])?\\d{0,3}(\\.|[- ])?\\d{0,4}", + // international + "011(\\.|[- \\d])*", +]; +const RE_PHONE_COMMIT = [ + // 555-1234 + "\\d{3}(\\.|[- ])?\\d{4}", + // 408 555-1234 + "\\d{3}(\\.|[- ])?\\d{3}(\\.|[- ])?\\d{4}", + // (408) 555-1234 + "\\(\\d{3}\\)(\\.|[- ])?\\d{3}(\\.|[- ])?\\d{4}", + // international + "011(\\.|[- \\d])*", +]; +const RE_SSN_ENTRY = ["\\d{0,3}(\\.|[- ])?\\d{0,2}(\\.|[- ])?\\d{0,4}"]; +const RE_SSN_COMMIT = ["\\d{3}(\\.|[- ])?\\d{2}(\\.|[- ])?\\d{4}"]; +const ADBE = { + Reader_Value_Asked: true, + Viewer_Value_Asked: true, +}; export { - Border, - Cursor, - Display, - Font, - GlobalConstants, - Highlight, - Position, - ScaleHow, - ScaleWhen, - Style, - Trans, - ZoomType, + ADBE, + border, + cursor, + display, + font, + highlight, + IDS_AM, + IDS_GREATER_THAN, + IDS_GT_AND_LT, + IDS_INVALID_DATE, + IDS_INVALID_DATE2, + IDS_INVALID_MONTH, + IDS_INVALID_VALUE, + IDS_LESS_THAN, + IDS_MONTH_INFO, + IDS_PM, + IDS_STARTUP_CONSOLE_MSG, + position, + RE_NUMBER_COMMIT_COMMA_SEP, + RE_NUMBER_COMMIT_DOT_SEP, + RE_NUMBER_ENTRY_COMMA_SEP, + RE_NUMBER_ENTRY_DOT_SEP, + RE_PHONE_COMMIT, + RE_PHONE_ENTRY, + RE_SSN_COMMIT, + RE_SSN_ENTRY, + RE_ZIP_COMMIT, + RE_ZIP_ENTRY, + RE_ZIP4_COMMIT, + RE_ZIP4_ENTRY, + scaleHow, + scaleWhen, + style, + trans, + zoomtype, }; diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 5b9c66d2bbc58..4673bf19464a2 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -13,11 +13,8 @@ * limitations under the License. */ -import { makeArr, makeMap, serializeError } from "./app_utils.js"; -import { createActionsMap } from "./common.js"; -import { PDFObject } from "./pdf_object.js"; -import { PrintParams } from "./print_params.js"; -import { ZoomType } from "./constants.js"; +import "./print_params.js"; +import { zoomtype } from "./constants.js"; const DOC_EXTERNAL = false; @@ -31,91 +28,177 @@ class InfoProxyHandler { } } -class Doc extends PDFObject { +globalThis.Doc = class Doc { + static claimInternals() { + delete Doc.claimInternals; + return instance => ({ + setCalculateNow(v) { + instance.#calculateNow = v; + }, + get calculate() { + return instance.#calculate; + }, + initActions: instance.#initActions.bind(instance), + dispatchDocEvent: instance.#dispatchDocEvent.bind(instance), + dispatchPageEvent: instance.#dispatchPageEvent.bind(instance), + getTerminalChildren: instance.#getTerminalChildren.bind(instance), + addField: instance.#addField.bind(instance), + }); + } + #pageActions = null; #otherPageActions = null; - constructor(data) { - super(data); + // Private fields only accessed within this class. + #baseURL; + + #calculate = true; + + #delay = false; + + #dirty = false; + + #disclosed = false; + + #media; + + #metadata; + + #noautocomplete; + + #nocache; + + #spellDictionaryOrder = []; + + #spellLanguageOrder = []; + + #printParams = null; + + #fields = new Map(); + + #fieldNames = []; + + #author; + + #creator; + + #creationDate; + + #docID; + + #documentFileName; + + #filesize; + + #keywords; + + #layout; + #modDate; + + #numFields = 0; + + #numPages; + + #pageNum; + + #producer; + + #securityHandler; + + #subject; + + #title; + + #URL; + + #info; + + #zoomType = zoomtype.none; + + #zoom; + + #actions; + + #globalEval; + + #getFieldPrivate; + + #send; + + #userActivationData; + + #calculateNow; + + constructor(data) { // 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(); - this._fieldNames = []; - this._event = null; - - this._author = data.Author || ""; - this._creator = data.Creator || ""; - this._creationDate = this._getDate(data.CreationDate) || null; - this._docID = data.docID || ["", ""]; - this._documentFileName = data.filename || ""; - this._filesize = data.filesize || 0; - this._keywords = data.Keywords || ""; - this._layout = data.layout || ""; - this._modDate = this._getDate(data.ModDate) || null; - this._numFields = 0; - this._numPages = data.numPages || 1; - this._pageNum = data.pageNum || 0; - this._producer = data.Producer || ""; - this._securityHandler = data.EncryptFilterName || null; - this._subject = data.Subject || ""; - this._title = data.Title || ""; - this._URL = data.URL || ""; + + this.#baseURL = data.baseURL || ""; + this.#metadata = data.metadata || ""; + this.#spellDictionaryOrder = []; + this.#spellLanguageOrder = []; + + this.#author = data.Author || ""; + this.#creator = data.Creator || ""; + this.#creationDate = this.#getDate(data.CreationDate) || null; + this.#docID = data.docID || ["", ""]; + this.#documentFileName = data.filename || ""; + this.#filesize = data.filesize || 0; + this.#keywords = data.Keywords || ""; + this.#layout = data.layout || ""; + this.#modDate = this.#getDate(data.ModDate) || null; + this.#numPages = data.numPages || 1; + this.#pageNum = data.pageNum || 0; + this.#producer = data.Producer || ""; + this.#securityHandler = data.EncryptFilterName || null; + this.#subject = data.Subject || ""; + this.#title = data.Title || ""; + this.#URL = data.URL || ""; // info has case insensitive properties // and they're are read-only. - this._info = new Proxy( + this.#info = new Proxy( { - title: this._title, - author: this._author, - authors: data.authors || [this._author], - subject: this._subject, - keywords: this._keywords, - creator: this._creator, - producer: this._producer, - creationdate: this._creationDate, - moddate: this._modDate, + title: this.#title, + author: this.#author, + authors: data.authors || [this.#author], + subject: this.#subject, + keywords: this.#keywords, + creator: this.#creator, + producer: this.#producer, + creationdate: this.#creationDate, + moddate: this.#modDate, trapped: data.Trapped || "Unknown", }, InfoProxyHandler ); - this._zoomType = ZoomType.none; - this._zoom = data.zoom || 100; - this._actions = createActionsMap(data.actions); - this._globalEval = data.globalEval; - this._userActivation = false; - this._disablePrinting = false; - this._disableSaving = false; + this.#zoom = data.zoom || 100; + this.#actions = new Map(data.actions ? Object.entries(data.actions) : null); + this.#globalEval = data.globalEval; + this.#getFieldPrivate = data.getFieldPrivate; + this.#send = data.send; + this.#userActivationData = data.userActivationData; + this.#printParams = new globalThis.PrintParams({ + lastPage: this.#numPages - 1, + }); + delete globalThis.PrintParams; } - _initActions() { - for (const { obj } of this._fields.values()) { + #initActions() { + for (const field of this.#fields.values()) { // Some fields may have compute their values so we need to send them // to the view. - const initialValue = obj._initialValue; + const fp = this.#getFieldPrivate(field); + const initialValue = fp.getInitialValue(field); if (initialValue) { - this._send({ - id: obj._id, - siblings: obj._siblings, + this.#send({ + id: fp.getId(field), + siblings: fp.getSiblings(field), value: initialValue, - formattedValue: obj.value.toString(), + formattedValue: field.value.toString(), }); } } @@ -133,92 +216,100 @@ class Doc extends PDFObject { // A pdf can contain an action /FooBar which will trigger a save // even if there are no WillSave/DidSave (which are themselves triggered // after a save). - this._disableSaving = true; - for (const actionName of this._actions.keys()) { + this.#userActivationData.disableSaving = true; + for (const actionName of this.#actions.keys()) { if (!dontRun.has(actionName)) { - this._runActions(actionName); + this.#runActions(actionName); } } - this._runActions("OpenAction"); - this._disableSaving = false; + this.#runActions("OpenAction"); + this.#userActivationData.disableSaving = false; } - _dispatchDocEvent(name) { + #dispatchDocEvent(name) { switch (name) { case "Open": - this._disableSaving = true; - this._runActions("OpenAction"); - this._disableSaving = false; + this.#userActivationData.disableSaving = true; + this.#runActions("OpenAction"); + this.#userActivationData.disableSaving = false; break; case "WillPrint": - this._disablePrinting = true; + this.#userActivationData.disablePrinting = true; try { - this._runActions(name); + this.#runActions(name); } catch (error) { - this._send(serializeError(error)); + this.#send(error); } - this._send({ command: "WillPrintFinished" }); - this._disablePrinting = false; + this.#send({ command: "WillPrintFinished" }); + this.#userActivationData.disablePrinting = false; break; case "WillSave": - this._disableSaving = true; - this._runActions(name); - this._disableSaving = false; + this.#userActivationData.disableSaving = true; + this.#runActions(name); + this.#userActivationData.disableSaving = false; break; default: - this._runActions(name); + this.#runActions(name); } } - _dispatchPageEvent(name, actions, pageNumber) { + #dispatchPageEvent(name, actions, pageNumber) { if (name === "PageOpen") { this.#pageActions ??= new Map(); if (!this.#pageActions.has(pageNumber)) { - this.#pageActions.set(pageNumber, createActionsMap(actions)); + this.#pageActions.set( + pageNumber, + new Map(actions ? Object.entries(actions) : null) + ); } - this._pageNum = pageNumber - 1; + this.#pageNum = pageNumber - 1; } for (const acts of [this.#pageActions, this.#otherPageActions]) { actions = acts?.get(pageNumber)?.get(name); if (actions) { for (const action of actions) { - this._globalEval(action); + this.#globalEval(action); } } } } - _runActions(name) { - const actions = this._actions.get(name); + #runActions(name) { + const actions = this.#actions.get(name); if (!actions) { return; } for (const action of actions) { try { - this._globalEval(action); + this.#globalEval(action); } catch (error) { - const serializedError = serializeError(error); - serializedError.value = `Error when executing "${name}" for document\n${serializedError.value}`; - this._send(serializedError); + this.#send({ + toString() { + return `Error when executing "${name}" for document\n${error.toString()}`; + }, + stack: error.stack, + }); } } } - _addField(name, field) { - this._fields.set(name, field); - this._fieldNames.push(name); - this._numFields++; + #addField(name, field) { + this.#fields.set(name, field); + this.#fieldNames.push(name); + this.#numFields++; // Fields on a page can have PageOpen/PageClose actions. - const po = field.obj._actions.get("PageOpen"); - const pc = field.obj._actions.get("PageClose"); + const fp = this.#getFieldPrivate(field); + const po = fp.getActions(field).get("PageOpen"); + const pc = fp.getActions(field).get("PageClose"); if (po || pc) { this.#otherPageActions ??= new Map(); const actions = this.#otherPageActions.getOrInsertComputed( - field.obj._page + 1, - makeMap + fp.getPage(field) + 1, + () => new Map() ); + const makeArr = () => []; if (po) { actions.getOrInsertComputed("PageOpen", makeArr).push(...po); } @@ -228,7 +319,7 @@ class Doc extends PDFObject { } } - _getDate(date) { + #getDate(date) { // date format is D:YYYYMMDDHHmmSS[OHH'mm'] if (!date || date.length < 15 || !date.startsWith("D:")) { return date; @@ -256,7 +347,7 @@ class Doc extends PDFObject { } get author() { - return this._author; + return this.#author; } set author(_) { @@ -264,11 +355,11 @@ class Doc extends PDFObject { } get baseURL() { - return this._baseURL; + return this.#baseURL; } set baseURL(baseURL) { - this._baseURL = baseURL; + this.#baseURL = baseURL; } get bookmarkRoot() { @@ -280,15 +371,15 @@ class Doc extends PDFObject { } get calculate() { - return this._calculate; + return this.#calculate; } set calculate(calculate) { - this._calculate = calculate; + this.#calculate = calculate; } get creator() { - return this._creator; + return this.#creator; } set creator(_) { @@ -304,31 +395,31 @@ class Doc extends PDFObject { } get delay() { - return this._delay; + return this.#delay; } set delay(delay) { - this._delay = delay; + this.#delay = delay; } get dirty() { - return this._dirty; + return this.#dirty; } set dirty(dirty) { - this._dirty = dirty; + this.#dirty = dirty; } get disclosed() { - return this._disclosed; + return this.#disclosed; } set disclosed(disclosed) { - this._disclosed = disclosed; + this.#disclosed = disclosed; } get docID() { - return this._docID; + return this.#docID; } set docID(_) { @@ -336,7 +427,7 @@ class Doc extends PDFObject { } get documentFileName() { - return this._documentFileName; + return this.#documentFileName; } set documentFileName(_) { @@ -363,7 +454,7 @@ class Doc extends PDFObject { } get filesize() { - return this._filesize; + return this.#filesize; } set filesize(_) { @@ -395,7 +486,7 @@ class Doc extends PDFObject { } get info() { - return this._info; + return this.#info; } set info(_) { @@ -427,7 +518,7 @@ class Doc extends PDFObject { } get keywords() { - return this._keywords; + return this.#keywords; } set keywords(_) { @@ -435,14 +526,14 @@ class Doc extends PDFObject { } get layout() { - return this._layout; + return this.#layout; } set layout(value) { - if (!this._userActivation) { + if (!this.#userActivationData.userActivation) { return; } - this._userActivation = false; + this.#userActivationData.userActivation = false; if (typeof value !== "string") { return; @@ -458,28 +549,28 @@ class Doc extends PDFObject { ) { value = "SinglePage"; } - this._send({ command: "layout", value }); - this._layout = value; + this.#send({ command: "layout", value }); + this.#layout = value; } get media() { - return this._media; + return this.#media; } set media(media) { - this._media = media; + this.#media = media; } get metadata() { - return this._metadata; + return this.#metadata; } set metadata(metadata) { - this._metadata = metadata; + this.#metadata = metadata; } get modDate() { - return this._modDate; + return this.#modDate; } set modDate(_) { @@ -503,23 +594,23 @@ class Doc extends PDFObject { } get noautocomplete() { - return this._noautocomplete; + return this.#noautocomplete; } set noautocomplete(noautocomplete) { - this._noautocomplete = noautocomplete; + this.#noautocomplete = noautocomplete; } get nocache() { - return this._nocache; + return this.#nocache; } set nocache(nocache) { - this._nocache = nocache; + this.#nocache = nocache; } get numFields() { - return this._numFields; + return this.#numFields; } set numFields(_) { @@ -527,7 +618,7 @@ class Doc extends PDFObject { } get numPages() { - return this._numPages; + return this.#numPages; } set numPages(_) { @@ -559,20 +650,20 @@ class Doc extends PDFObject { } get pageNum() { - return this._pageNum; + return this.#pageNum; } set pageNum(value) { - if (!this._userActivation) { + if (!this.#userActivationData.userActivation) { return; } - this._userActivation = false; + this.#userActivationData.userActivation = false; - if (typeof value !== "number" || value < 0 || value >= this._numPages) { + if (typeof value !== "number" || value < 0 || value >= this.#numPages) { return; } - this._send({ command: "page-num", value }); - this._pageNum = value; + this.#send({ command: "page-num", value }); + this.#pageNum = value; } get pageWindowRect() { @@ -600,7 +691,7 @@ class Doc extends PDFObject { } get producer() { - return this._producer; + return this.#producer; } set producer(_) { @@ -616,7 +707,7 @@ class Doc extends PDFObject { } get securityHandler() { - return this._securityHandler; + return this.#securityHandler; } set securityHandler(_) { @@ -640,23 +731,23 @@ class Doc extends PDFObject { } get spellDictionaryOrder() { - return this._spellDictionaryOrder; + return this.#spellDictionaryOrder; } set spellDictionaryOrder(spellDictionaryOrder) { - this._spellDictionaryOrder = spellDictionaryOrder; + this.#spellDictionaryOrder = spellDictionaryOrder; } get spellLanguageOrder() { - return this._spellLanguageOrder; + return this.#spellLanguageOrder; } set spellLanguageOrder(spellLanguageOrder) { - this._spellLanguageOrder = spellLanguageOrder; + this.#spellLanguageOrder = spellLanguageOrder; } get subject() { - return this._subject; + return this.#subject; } set subject(_) { @@ -672,7 +763,7 @@ class Doc extends PDFObject { } get title() { - return this._title; + return this.#title; } set title(_) { @@ -680,7 +771,7 @@ class Doc extends PDFObject { } get URL() { - return this._URL; + return this.#URL; } set URL(_) { @@ -696,7 +787,7 @@ class Doc extends PDFObject { } get xfa() { - return this._xfa; + return undefined; } set xfa(_) { @@ -712,59 +803,59 @@ class Doc extends PDFObject { } get zoomType() { - return this._zoomType; + return this.#zoomType; } set zoomType(type) { - if (!this._userActivation) { + if (!this.#userActivationData.userActivation) { return; } - this._userActivation = false; + this.#userActivationData.userActivation = false; if (typeof type !== "string") { return; } switch (type) { - case ZoomType.none: - this._send({ command: "zoom", value: 1 }); + case zoomtype.none: + this.#send({ command: "zoom", value: 1 }); break; - case ZoomType.fitP: - this._send({ command: "zoom", value: "page-fit" }); + case zoomtype.fitP: + this.#send({ command: "zoom", value: "page-fit" }); break; - case ZoomType.fitW: - this._send({ command: "zoom", value: "page-width" }); + case zoomtype.fitW: + this.#send({ command: "zoom", value: "page-width" }); break; - case ZoomType.fitH: - this._send({ command: "zoom", value: "page-height" }); + case zoomtype.fitH: + this.#send({ command: "zoom", value: "page-height" }); break; - case ZoomType.fitV: - this._send({ command: "zoom", value: "auto" }); + case zoomtype.fitV: + this.#send({ command: "zoom", value: "auto" }); break; - case ZoomType.pref: - case ZoomType.refW: + case zoomtype.pref: + case zoomtype.refW: break; default: return; } - this._zoomType = type; + this.#zoomType = type; } get zoom() { - return this._zoom; + return this.#zoom; } set zoom(value) { - if (!this._userActivation) { + if (!this.#userActivationData.userActivation) { return; } - this._userActivation = false; + this.#userActivationData.userActivation = false; if (typeof value !== "number" || value < 8.33 || value > 6400) { return; } - this._send({ command: "zoom", value: value / 100 }); + this.#send({ command: "zoom", value: value / 100 }); } addAnnot() { @@ -816,7 +907,7 @@ class Doc extends PDFObject { } calculateNow() { - this._eventDispatcher.calculateNow(); + this.#calculateNow(); } closeDoc() { @@ -923,14 +1014,14 @@ class Doc extends PDFObject { /* Not implemented */ } - _getField(cName) { + #getField(cName) { if (cName && typeof cName === "object") { cName = cName.cName; } if (typeof cName !== "string") { throw new TypeError("Invalid field name: must be a string"); } - const searchedField = this._fields.get(cName); + const searchedField = this.#fields.get(cName); if (searchedField) { return searchedField; } @@ -942,19 +1033,19 @@ class Doc extends PDFObject { cName = parts[0]; } - for (const [name, field] of this._fields) { + for (const [name, field] of this.#fields) { if (name.endsWith(cName)) { if (!isNaN(childIndex)) { - const children = this._getChildren(name); + const children = this.#getChildren(name); if (childIndex < 0 || childIndex >= children.length) { childIndex = 0; } if (childIndex < children.length) { - this._fields.set(cName, children[childIndex]); + this.#fields.set(cName, children[childIndex]); return children[childIndex]; } } - this._fields.set(cName, field); + this.#fields.set(cName, field); return field; } } @@ -963,20 +1054,20 @@ class Doc extends PDFObject { } getField(cName) { - const field = this._getField(cName); + const field = this.#getField(cName); if (!field) { return null; } - return field.wrapped; + return field; } - _getChildren(fieldName) { + #getChildren(fieldName) { // Children of foo.bar are foo.bar.oof, foo.bar.rab // but not foo.bar.oof.FOO. const len = fieldName.length; const children = []; const pattern = /^\.[^.]+$/; - for (const [name, field] of this._fields) { + for (const [name, field] of this.#fields) { if (name.startsWith(fieldName)) { const finalPart = name.slice(len); if (pattern.test(finalPart)) { @@ -987,18 +1078,18 @@ class Doc extends PDFObject { return children; } - _getTerminalChildren(fieldName) { + #getTerminalChildren(fieldName) { // Get all the descendants which have a value. const children = []; const len = fieldName.length; - for (const [name, field] of this._fields) { + for (const [name, field] of this.#fields) { if (name.startsWith(fieldName)) { const finalPart = name.slice(len); if ( - field.obj._hasValue && + this.#getFieldPrivate(field).getHasValue(field) && (finalPart === "" || finalPart.startsWith(".")) ) { - children.push(field.wrapped); + children.push(field); } } } @@ -1025,7 +1116,7 @@ class Doc extends PDFObject { throw new TypeError("Invalid field index: must be a number"); } if (0 <= nIndex && nIndex < this.numFields) { - return this._fieldNames[Math.trunc(nIndex)]; + return this.#fieldNames[Math.trunc(nIndex)]; } return null; } @@ -1071,9 +1162,7 @@ class Doc extends PDFObject { } getPrintParams() { - return (this._printParams ||= new PrintParams({ - lastPage: this._numPages - 1, - })); + return this.#printParams; } getSound() { @@ -1155,10 +1244,13 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { - if (this._disablePrinting || !this._userActivation) { + if ( + this.#userActivationData.disablePrinting || + !this.#userActivationData.userActivation + ) { return; } - this._userActivation = false; + this.#userActivationData.userActivation = false; if (bUI && typeof bUI === "object") { nStart = bUI.nStart; @@ -1184,7 +1276,7 @@ class Doc extends PDFObject { nEnd = typeof nEnd === "number" ? Math.max(0, Math.trunc(nEnd)) : -1; - this._send({ command: "print", start: nStart, end: nEnd }); + this.#send({ command: "print", start: nStart, end: nEnd }); } removeDataObject() { @@ -1250,7 +1342,7 @@ class Doc extends PDFObject { fieldsToReset = null; break; } - const field = this._getField(fieldName); + const field = this.#getField(fieldName); if (!field) { continue; } @@ -1260,16 +1352,16 @@ class Doc extends PDFObject { } if (!fieldsToReset) { - fieldsToReset = this._fields.values(); - mustCalculate = this._fields.size !== 0; + fieldsToReset = this.#fields.values(); + mustCalculate = this.#fields.size !== 0; } for (const field of fieldsToReset) { - field.obj.value = field.obj.defaultValue; - this._send({ - id: field.obj._id, - siblings: field.obj._siblings, - value: field.obj.defaultValue, + field.value = field.defaultValue; + this.#send({ + id: this.#getFieldPrivate(field).getId(field), + siblings: this.#getFieldPrivate(field).getSiblings(field), + value: field.defaultValue, formattedValue: null, selRange: [0, 0], }); @@ -1339,6 +1431,6 @@ class Doc extends PDFObject { syncAnnotScan() { /* Not implemented */ } -} +}; -export { Doc }; +export {}; diff --git a/src/scripting_api/error.js b/src/scripting_api/error.js deleted file mode 100644 index 4345999162048..0000000000000 --- a/src/scripting_api/error.js +++ /dev/null @@ -1,23 +0,0 @@ -/* Copyright 2020 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -class NotSupportedError extends Error { - constructor(name) { - super(`${name} isn't supported in PDF.js`); - this.name = "NotSupportedError"; - } -} - -export { NotSupportedError }; diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index f84d76688934a..00e794d53246c 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -13,11 +13,6 @@ * limitations under the License. */ -import { - USERACTIVATION_CALLBACKID, - USERACTIVATION_MAXTIME_VALIDITY, -} from "./app_utils.js"; - class Event { constructor(data) { this.change = data.change || ""; @@ -43,18 +38,52 @@ class Event { } } -class EventDispatcher { - constructor(document, calculationOrder, objects, externalCall) { - this._document = document; - this._calculationOrder = calculationOrder; - this._objects = objects; - this._externalCall = externalCall; +globalThis.EventDispatcher = class EventDispatcher { + static claimInternals() { + delete EventDispatcher.claimInternals; + return instance => ({ + dispatch: instance.#dispatch.bind(instance), + mergeChange: instance.#mergeChange.bind(instance), + }); + } + + #doc; + + #calculationOrder; + + #objects; + + #externalCall; + + #getFieldPrivate; - this._document.obj._eventDispatcher = this; - this._isCalculating = false; + #docInternals; + + #isCalculating = false; + + #userActivationData; + + constructor( + doc, + calculationOrder, + objects, + externalCall, + getFieldPrivate, + docInternals, + userActivationData + ) { + this.#doc = doc; + this.#calculationOrder = calculationOrder; + this.#objects = objects; + this.#externalCall = externalCall; + this.#getFieldPrivate = getFieldPrivate; + this.#docInternals = docInternals; + this.#userActivationData = userActivationData; + + docInternals.setCalculateNow(this.#calculateNow.bind(this)); } - mergeChange(event) { + #mergeChange(event) { let value = event.value; if (Array.isArray(value)) { return value; @@ -72,30 +101,33 @@ class EventDispatcher { return `${prefix}${event.change}${postfix}`; } - userActivation() { - this._document.obj._userActivation = true; - this._externalCall("setTimeout", [ + #userActivation() { + this.#userActivationData.userActivation = true; + + const USERACTIVATION_CALLBACKID = 0; + const USERACTIVATION_MAXTIME_VALIDITY = 5000; + this.#externalCall("setTimeout", [ USERACTIVATION_CALLBACKID, USERACTIVATION_MAXTIME_VALIDITY, ]); } - dispatch(baseEvent) { + #dispatch(baseEvent) { if ( typeof PDFJSDev !== "undefined" && PDFJSDev.test("TESTING") && baseEvent.name === "sandboxtripbegin" ) { - this._externalCall("send", [{ command: "sandboxTripEnd" }]); + this.#externalCall("send", [{ command: "sandboxTripEnd" }]); return; } const id = baseEvent.id; - if (!(id in this._objects)) { + if (!(id in this.#objects)) { let event; if (id === "doc" || id === "page") { event = globalThis.event = new Event(baseEvent); - event.source = event.target = this._document.wrapped; + event.source = event.target = globalThis; event.name = baseEvent.name; } if (id === "doc") { @@ -103,11 +135,11 @@ class EventDispatcher { if (eventName === "Open") { // The user has decided to open this pdf, hence we enable // userActivation. - this.userActivation(); + this.#userActivation(); // Initialize named actions before calling formatAll to avoid any // errors in the case where a formatter is using one of those named // actions (see #15818). - this._document.obj._initActions(); + this.#docInternals.initActions(); // Before running the Open event, we run the format callbacks but // without changing the value of the fields. // Acrobat does the same thing. @@ -116,38 +148,41 @@ class EventDispatcher { if ( !["DidPrint", "DidSave", "WillPrint", "WillSave"].includes(eventName) ) { - this.userActivation(); + this.#userActivation(); } - this._document.obj._dispatchDocEvent(event.name); + this.#docInternals.dispatchDocEvent(event.name); } else if (id === "page") { - this.userActivation(); - this._document.obj._dispatchPageEvent( + this.#userActivation(); + this.#docInternals.dispatchPageEvent( event.name, baseEvent.actions, baseEvent.pageNumber ); } else if (id === "app" && baseEvent.name === "ResetForm") { - this.userActivation(); + this.#userActivation(); for (const fieldId of baseEvent.ids) { - const obj = this._objects[fieldId]; - obj?.obj._reset(); + const field = this.#objects[fieldId]; + if (field) { + this.#getFieldPrivate(field).reset(field); + } } } return; } const name = baseEvent.name; - const source = this._objects[id]; + const source = this.#objects[id]; const event = (globalThis.event = new Event(baseEvent)); let savedChange; - this.userActivation(); + this.#userActivation(); - if (source.obj._isButton()) { - source.obj._id = id; - event.value = source.obj._getExportValue(event.value); + const sfp = this.#getFieldPrivate(source); + if (sfp.isButton()) { + sfp.setId(source, id); + event.value = sfp.getExportValue(source, event.value); if (name === "Action") { - source.obj._value = event.value; + sfp.setValue(source, event.value); } } @@ -189,17 +224,17 @@ class EventDispatcher { if (event.willCommit) { this.runValidation(source, event); } else { - if (source.obj._isChoice) { - source.obj.value = savedChange.changeEx; - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, - value: source.obj.value, + if (sfp.getIsChoice(source)) { + source.value = savedChange.changeEx; + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), + value: source.value, }); return; } - const value = (source.obj.value = this.mergeChange(event)); + const value = (source.value = this.#mergeChange(event)); let selStart, selEnd; if ( event.selStart !== savedChange.selStart || @@ -211,26 +246,26 @@ class EventDispatcher { } else { selEnd = selStart = savedChange.selStart + event.change.length; } - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), value, selRange: [selStart, selEnd], }); } } else if (!event.willCommit) { - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), value: savedChange.value, selRange: [savedChange.selStart, savedChange.selEnd], }); } else { // Entry is not valid (rc == false) and it's a commit // so just clear the field. - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), value: "", formattedValue: null, selRange: [0, 0], @@ -241,38 +276,39 @@ class EventDispatcher { formatAll() { // Run format actions if any for all the fields. const event = (globalThis.event = new Event({})); - for (const source of Object.values(this._objects)) { - event.value = source.obj._getValue(); + for (const source of Object.values(this.#objects)) { + event.value = this.#getFieldPrivate(source).getComputedValue(source); this.runActions(source, source, event, "Format"); } } runValidation(source, event) { const didValidateRun = this.runActions(source, source, event, "Validate"); + const sfp = this.#getFieldPrivate(source); if (event.rc) { - source.obj.value = event.value; + source.value = event.value; this.runCalculate(source, event); - const savedValue = (event.value = source.obj._getValue()); + const savedValue = (event.value = sfp.getComputedValue(source)); let formattedValue = null; if (this.runActions(source, source, event, "Format")) { formattedValue = event.value?.toString?.(); } - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), value: savedValue, formattedValue, }); event.value = savedValue; } else if (didValidateRun) { // The value is not valid. - source.obj._send({ - id: source.obj._id, - siblings: source.obj._siblings, + sfp.send(source, { + id: sfp.getId(source), + siblings: sfp.getSiblings(source), value: "", formattedValue: null, selRange: [0, 0], @@ -282,63 +318,64 @@ class EventDispatcher { } runActions(source, target, event, eventName) { - event.source = source.wrapped; - event.target = target.wrapped; + event.source = source; + event.target = target; event.name = eventName; - event.targetName = target.obj.name; + event.targetName = target.name; event.rc = true; - return target.obj._runActions(event); + return this.#getFieldPrivate(target).runActions(target, event); } - calculateNow() { + #calculateNow() { // This function can be called by a JS script (doc.calculateNow()). - // If !this._calculationOrder then there is nothing to calculate. - // _isCalculating is here to prevent infinite recursion with calculateNow. - // If !this._document.obj.calculate then the script doesn't want to have + // If !this.#calculationOrder then there is nothing to calculate. + // #isCalculating is here to prevent infinite recursion with calculateNow. + // If !this.#doc.calculate then the script doesn't want to have // a calculate. if ( - !this._calculationOrder || - this._isCalculating || - !this._document.obj.calculate + !this.#calculationOrder || + this.#isCalculating || + !this.#doc.calculate ) { return; } - this._isCalculating = true; - const first = this._calculationOrder[0]; - const source = this._objects[first]; + this.#isCalculating = true; + const first = this.#calculationOrder[0]; + const source = this.#objects[first]; globalThis.event = new Event({}); this.runCalculate(source, globalThis.event); - this._isCalculating = false; + this.#isCalculating = false; } runCalculate(source, event) { - // _document.obj.calculate is equivalent to doc.calculate and can be + // #doc.calculate is equivalent to doc.calculate and can be // changed by a script to allow a future calculate or not. // This function is either called by calculateNow or when an action // is triggered (in this case we cannot be currently calculating). - // So there are no need to check for _isCalculating because it has + // So there are no need to check for #isCalculating because it has // been already done in calculateNow. - if (!this._calculationOrder || !this._document.obj.calculate) { + if (!this.#calculationOrder || !this.#doc.calculate) { return; } - for (const targetId of this._calculationOrder) { - if (!(targetId in this._objects)) { + for (const targetId of this.#calculationOrder) { + if (!(targetId in this.#objects)) { continue; } - if (!this._document.obj.calculate) { + if (!this.#doc.calculate) { // An action could have changed calculate value. break; } event.value = null; - const target = this._objects[targetId]; - let savedValue = target.obj._getValue(); + const target = this.#objects[targetId]; + const tfp = this.#getFieldPrivate(target); + let savedValue = tfp.getComputedValue(target); this.runActions(source, target, event, "Calculate"); if (!event.rc) { @@ -347,37 +384,37 @@ class EventDispatcher { if (event.value !== null) { // A new value has been calculated so set it. - target.obj.value = event.value; + target.value = event.value; } else { - event.value = target.obj._getValue(); + event.value = tfp.getComputedValue(target); } this.runActions(target, target, event, "Validate"); if (!event.rc) { - if (target.obj._getValue() !== savedValue) { - target.wrapped.value = savedValue; + if (tfp.getComputedValue(target) !== savedValue) { + target.value = savedValue; } continue; } if (event.value === null) { - event.value = target.obj._getValue(); + event.value = tfp.getComputedValue(target); } - savedValue = target.obj._getValue(); + savedValue = tfp.getComputedValue(target); let formattedValue = null; if (this.runActions(target, target, event, "Format")) { formattedValue = event.value?.toString?.(); } - target.obj._send({ - id: target.obj._id, - siblings: target.obj._siblings, + tfp.send(target, { + id: tfp.getId(target), + siblings: tfp.getSiblings(target), value: savedValue, formattedValue, }); } } -} +}; -export { Event, EventDispatcher }; +export {}; diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 0834bc6bbd46f..1be9e2823ffaa 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -13,14 +13,87 @@ * limitations under the License. */ -import { createActionsMap, FieldType, getFieldType } from "./common.js"; -import { Color } from "./color.js"; -import { PDFObject } from "./pdf_object.js"; -import { serializeError } from "./app_utils.js"; +globalThis.Field = class Field { + #browseForFileToSubmit; + + #buttonCaption = null; + + #buttonIcon = null; + + #charLimit; + + #children = null; + + #currentValueIndices; + + #display; + + #editable; + + #fieldPath; + + #fillColor; + + #hidden; + + #items; + + #originalValue; + + #print; + + #readonly; + + #required; + + #rotation; + + #strokeColor; + + #textColor; + + #userName; + + #datetimeFormat; + + #hasDateOrTime; + + #util; + + #globalEval; + + #appObjects; + + #fieldType; + + #kidIds; + + #id; + + #send; + + #actions; + + #hasValue; + + #isChoice; + + #page; + + #siblings; + + #value = null; + + #initialized = false; + + // Capability object and accessors passed in from initSandbox. + #fp; + + #getFieldPrivate; + + #docInternals; -class Field extends PDFObject { constructor(data) { - super(data); this.alignment = data.alignment || "left"; this.borderStyle = data.borderStyle || ""; this.buttonAlignX = data.buttonAlignX || 50; @@ -38,23 +111,17 @@ class Field extends PDFObject { this.doNotScroll = data.doNotScroll; this.doNotSpellCheck = data.doNotSpellCheck; this.delay = data.delay; - this.display = data.display; - this.doc = data.doc.wrapped; - this.editable = data.editable; + this.doc = globalThis; this.exportValues = data.exportValues; this.fileSelect = data.fileSelect; - this.hidden = data.hidden; this.highlight = data.highlight; this.lineWidth = data.lineWidth; this.multiline = data.multiline; this.multipleSelection = !!data.multipleSelection; this.name = data.name; this.password = data.password; - this.print = data.print; this.radiosInUnison = data.radiosInUnison; - this.readonly = data.readonly; this.rect = data.rect; - this.required = data.required; this.richText = data.richText; this.richValue = data.richValue; this.style = data.style; @@ -62,50 +129,121 @@ class Field extends PDFObject { this.textFont = data.textFont; this.textSize = data.textSize; this.type = data.type; - this.userName = data.userName; + + // Stored in private fields so that their setters (which call #send) + // are NOT triggered during construction. + this.#display = data.display; + this.#editable = data.editable; + this.#hidden = data.hidden; + this.#print = data.print; + this.#readonly = data.readonly; + this.#required = data.required; + this.#userName = data.userName; // Private - this._actions = createActionsMap(data.actions); - this._browseForFileToSubmit = data.browseForFileToSubmit || null; - this._buttonCaption = null; - this._buttonIcon = null; - this._charLimit = data.charLimit; - this._children = null; - this._currentValueIndices = data.currentValueIndices || 0; - this._document = data.doc; - this._fieldPath = data.fieldPath; - this._fillColor = data.fillColor || ["T"]; - this._isChoice = Array.isArray(data.items); - this._items = data.items || []; - this._hasValue = Object.hasOwn(data, "value"); - this._page = data.page || 0; - this._strokeColor = data.strokeColor || ["G", 0]; - this._textColor = data.textColor || ["G", 0]; - this._value = null; - this._kidIds = data.kidIds || null; - this._fieldType = getFieldType(this._actions); - this._siblings = data.siblings || null; - this._rotation = data.rotation || 0; - this._datetimeFormat = data.datetimeFormat || null; - this._hasDateOrTime = !!data.hasDatetimeHTML; - this._util = data.util; - - this._globalEval = data.globalEval; - this._appObjects = data.appObjects; + this.#id = data.id; + this.#send = data.send ?? (() => {}); + this.#actions = new Map(data.actions ? Object.entries(data.actions) : null); + this.#browseForFileToSubmit = data.browseForFileToSubmit || null; + this.#charLimit = data.charLimit; + this.#currentValueIndices = data.currentValueIndices || 0; + this.#fieldPath = data.fieldPath; + this.#fillColor = data.fillColor || ["T"]; + this.#isChoice = Array.isArray(data.items); + this.#items = data.items || []; + this.#hasValue = Object.hasOwn(data, "value"); + this.#page = data.page || 0; + this.#strokeColor = data.strokeColor || ["G", 0]; + this.#textColor = data.textColor || ["G", 0]; + this.#kidIds = data.kidIds || null; + this.#fieldType = Field.#getFieldType(this.#actions); + this.#siblings = data.siblings || null; + this.#rotation = data.rotation || 0; + this.#datetimeFormat = data.datetimeFormat || null; + this.#hasDateOrTime = !!data.hasDatetimeHTML; + this.#util = data.util; + + this.#globalEval = data.globalEval; + this.#appObjects = data.appObjects; + this.#fp = data.fieldPrivate; + this.#getFieldPrivate = data.getFieldPrivate; + this.#docInternals = data.docInternals; // The value is set depending on the field type. this.value = data.value || ""; + this.#initialized = true; + } + + static claimInternals() { + delete Field.claimInternals; + return { + getId: f => f.#id, + setId: (f, v) => { + f.#id = v; + }, + send: (f, data) => { + f.#send(data); + }, + getActions: f => f.#actions, + setActions: (f, v) => { + f.#actions = v; + }, + getHasValue: f => f.#hasValue, + getIsChoice: f => f.#isChoice, + getPage: f => f.#page, + getSiblings: f => f.#siblings, + setSiblings: (f, v) => { + f.#siblings = v; + }, + getValue: f => f.#value, + setValue: (f, v) => { + f.#value = v; + }, + getInitialValue: f => (f.#hasDateOrTime && f.#originalValue) || null, + getKidIds: f => f.#kidIds, + getComputedValue: f => f.#originalValue ?? f.value, + isButton: () => false, + getExportValue: (_f, _state) => undefined, + reset: f => { + f.value = f.defaultValue; + }, + runActions: (f, event) => f.#runActions(event), + }; + } + + static #getFieldType(actions) { + let format = actions.get("Format"); + if (!format) { + return "none"; + } + + format = format[0]; + + format = format.trim(); + if (format.startsWith("AFNumber_")) { + return "number"; + } + if (format.startsWith("AFPercent_")) { + return "percent"; + } + if (format.startsWith("AFDate_")) { + return "date"; + } + if (format.startsWith("AFTime_")) { + return "time"; + } + return "none"; } get currentValueIndices() { - if (!this._isChoice) { + if (!this.#isChoice) { return 0; } - return this._currentValueIndices; + return this.#currentValueIndices; } set currentValueIndices(indices) { - if (!this._isChoice) { + if (!this.#isChoice) { return; } if (!Array.isArray(indices)) { @@ -126,53 +264,121 @@ class Field extends PDFObject { indices.sort(); if (this.multipleSelection) { - this._currentValueIndices = indices; - this._value = []; + this.#currentValueIndices = indices; + this.#value = []; indices.forEach(i => { - this._value.push(this._items[i].displayValue); + this.#value.push(this.#items[i].displayValue); }); } else if (indices.length > 0) { indices = indices.splice(1, indices.length - 1); - this._currentValueIndices = indices[0]; - this._value = this._items[this._currentValueIndices]; + this.#currentValueIndices = indices[0]; + this.#value = this.#items[this.#currentValueIndices]; } - this._send({ id: this._id, indices }); + this.#send({ id: this.#id, indices }); } get fillColor() { - return this._fillColor; + return this.#fillColor; } set fillColor(color) { - if (Color._isValidColor(color)) { - this._fillColor = color; + if (globalThis.color._isValidColor(color)) { + this.#fillColor = color; + this.#sendData({ id: this.#id, fillColor: color }); } } get bgColor() { - return this.fillColor; + return this.#fillColor; } set bgColor(color) { - this.fillColor = color; + if (globalThis.color._isValidColor(color)) { + this.#fillColor = color; + this.#sendData({ id: this.#id, bgColor: color }); + } } get charLimit() { - return this._charLimit; + return this.#charLimit; } set charLimit(limit) { if (typeof limit !== "number") { throw new Error("Invalid argument value"); } - this._charLimit = Math.max(0, Math.floor(limit)); + this.#charLimit = Math.max(0, Math.floor(limit)); + this.#sendData({ id: this.#id, charLimit: this.#charLimit }); + } + + get display() { + return this.#display; + } + + set display(value) { + this.#display = value; + this.#sendData({ id: this.#id, display: value }); + } + + get editable() { + return this.#editable; + } + + set editable(value) { + this.#editable = value; + this.#sendData({ id: this.#id, editable: value }); + } + + get hidden() { + return this.#hidden; + } + + set hidden(value) { + this.#hidden = value; + this.#sendData({ id: this.#id, hidden: value }); + } + + get print() { + return this.#print; + } + + set print(value) { + this.#print = value; + this.#sendData({ id: this.#id, print: value }); + } + + get readonly() { + return this.#readonly; + } + + set readonly(value) { + this.#readonly = value; + this.#sendData({ id: this.#id, readonly: value }); + } + + get required() { + return this.#required; + } + + set required(value) { + this.#required = value; + this.#sendData({ id: this.#id, required: value }); + } + + get userName() { + return this.#userName; + } + + set userName(value) { + this.#userName = value; + this.#sendData({ id: this.#id, userName: value }); } get numItems() { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } - return this._items.length; + return this.#items.length; } set numItems(_) { @@ -180,25 +386,29 @@ class Field extends PDFObject { } get strokeColor() { - return this._strokeColor; + return this.#strokeColor; } set strokeColor(color) { - if (Color._isValidColor(color)) { - this._strokeColor = color; + if (globalThis.color._isValidColor(color)) { + this.#strokeColor = color; + this.#sendData({ id: this.#id, strokeColor: color }); } } get borderColor() { - return this.strokeColor; + return this.#strokeColor; } set borderColor(color) { - this.strokeColor = color; + if (globalThis.color._isValidColor(color)) { + this.#strokeColor = color; + this.#sendData({ id: this.#id, borderColor: color }); + } } get page() { - return this._page; + return this.#page; } set page(_) { @@ -206,7 +416,7 @@ class Field extends PDFObject { } get rotation() { - return this._rotation; + return this.#rotation; } set rotation(angle) { @@ -218,106 +428,106 @@ class Field extends PDFObject { if (angle < 0) { angle += 360; } - this._rotation = angle; + this.#rotation = angle; + this.#sendData({ id: this.#id, rotation: angle }); } get textColor() { - return this._textColor; + return this.#textColor; } set textColor(color) { - if (Color._isValidColor(color)) { - this._textColor = color; + if (globalThis.color._isValidColor(color)) { + this.#textColor = color; + this.#sendData({ id: this.#id, textColor: color }); } } get fgColor() { - return this.textColor; + return this.#textColor; } set fgColor(color) { - this.textColor = color; + if (globalThis.color._isValidColor(color)) { + this.#textColor = color; + this.#sendData({ id: this.#id, fgColor: color }); + } } get value() { - return this._value; + return this.#value; } set value(value) { - if (this._isChoice) { - this._setChoiceValue(value); - return; - } - - if (this._hasDateOrTime && value) { - const date = this._util.scand(this._datetimeFormat, value); + if (this.#isChoice) { + this.#setChoiceValue(value); + } else if (this.#hasDateOrTime && value) { + const date = this.#util.scand(this.#datetimeFormat, value); if (date) { - this._originalValue = date.valueOf(); - value = this._util.printd(this._datetimeFormat, date); - this._value = !isNaN(value) ? parseFloat(value) : value; - return; + this.#originalValue = date.valueOf(); + value = this.#util.printd(this.#datetimeFormat, date); + this.#value = !isNaN(value) ? parseFloat(value) : value; + } else { + this.#originalValue = value; + const _value = value.trim().replace(",", "."); + this.#value = !isNaN(_value) ? parseFloat(_value) : value; } - } - - if ( + } else if ( value === "" || typeof value !== "string" || // When the field type is date or time, the value must be a string. - this._fieldType >= FieldType.date + ["date", "time"].includes(this.#fieldType) ) { - this._originalValue = undefined; - this._value = value; - return; + this.#originalValue = undefined; + this.#value = value; + } else { + this.#originalValue = value; + const _value = value.trim().replace(",", "."); + this.#value = !isNaN(_value) ? parseFloat(_value) : value; + } + if (this.#initialized) { + this.#sendData({ + id: this.#id, + value: this.#originalValue ?? this.#value, + }); } - - this._originalValue = value; - const _value = value.trim().replace(",", "."); - this._value = !isNaN(_value) ? parseFloat(_value) : value; - } - - get _initialValue() { - return (this._hasDateOrTime && this._originalValue) || null; - } - - _getValue() { - return this._originalValue ?? this.value; } - _setChoiceValue(value) { + #setChoiceValue(value) { if (this.multipleSelection) { if (!Array.isArray(value)) { value = [value]; } const values = new Set(value); - if (Array.isArray(this._currentValueIndices)) { - this._currentValueIndices.length = 0; - this._value.length = 0; + if (Array.isArray(this.#currentValueIndices)) { + this.#currentValueIndices.length = 0; + this.#value.length = 0; } else { - this._currentValueIndices = []; - this._value = []; + this.#currentValueIndices = []; + this.#value = []; } - this._items.forEach((item, i) => { + this.#items.forEach((item, i) => { if (values.has(item.exportValue)) { - this._currentValueIndices.push(i); - this._value.push(item.exportValue); + this.#currentValueIndices.push(i); + this.#value.push(item.exportValue); } }); } else { if (Array.isArray(value)) { value = value[0]; } - const index = this._items.findIndex( + const index = this.#items.findIndex( ({ exportValue }) => value === exportValue ); if (index !== -1) { - this._currentValueIndices = index; - this._value = this._items[index].exportValue; + this.#currentValueIndices = index; + this.#value = this.#items[index].exportValue; } } } get valueAsString() { - return (this._value ?? "").toString(); + return (this.#value ?? "").toString(); } set valueAsString(_) { @@ -325,24 +535,24 @@ class Field extends PDFObject { } browseForFileToSubmit() { - if (this._browseForFileToSubmit) { + if (this.#browseForFileToSubmit) { // TODO: implement this function on Firefox side // we can use nsIFilePicker but open method is async. // Maybe it's possible to use a html input (type=file) too. - this._browseForFileToSubmit(); + this.#browseForFileToSubmit(); } } buttonGetCaption(nFace = 0) { - if (this._buttonCaption) { - return this._buttonCaption[nFace]; + if (this.#buttonCaption) { + return this.#buttonCaption[nFace]; } return ""; } buttonGetIcon(nFace = 0) { - if (this._buttonIcon) { - return this._buttonIcon[nFace]; + if (this.#buttonIcon) { + return this.#buttonIcon[nFace]; } return null; } @@ -352,10 +562,10 @@ class Field extends PDFObject { } buttonSetCaption(cCaption, nFace = 0) { - if (!this._buttonCaption) { - this._buttonCaption = ["", "", ""]; + if (!this.#buttonCaption) { + this.#buttonCaption = ["", "", ""]; } - this._buttonCaption[nFace] = cCaption; + this.#buttonCaption[nFace] = cCaption; // TODO: send to the annotation layer // Right now the button is drawn on the canvas using its appearance so // update the caption means redraw... @@ -363,24 +573,24 @@ class Field extends PDFObject { } buttonSetIcon(oIcon, nFace = 0) { - if (!this._buttonIcon) { - this._buttonIcon = [null, null, null]; + if (!this.#buttonIcon) { + this.#buttonIcon = [null, null, null]; } - this._buttonIcon[nFace] = oIcon; + this.#buttonIcon[nFace] = oIcon; } checkThisBox(nWidget, bCheckIt = true) {} clearItems() { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } - this._items = []; - this._send({ id: this._id, clear: null }); + this.#items = []; + this.#send({ id: this.#id, clear: null }); } deleteItemAt(nIdx = null) { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } if (!this.numItems) { @@ -389,9 +599,9 @@ class Field extends PDFObject { if (nIdx === null) { // Current selected item. - nIdx = Array.isArray(this._currentValueIndices) - ? this._currentValueIndices[0] - : this._currentValueIndices; + nIdx = Array.isArray(this.#currentValueIndices) + ? this.#currentValueIndices[0] + : this.#currentValueIndices; nIdx ||= 0; } @@ -399,62 +609,64 @@ class Field extends PDFObject { nIdx = this.numItems - 1; } - this._items.splice(nIdx, 1); - if (Array.isArray(this._currentValueIndices)) { - let index = this._currentValueIndices.findIndex(i => i >= nIdx); + this.#items.splice(nIdx, 1); + if (Array.isArray(this.#currentValueIndices)) { + let index = this.#currentValueIndices.findIndex(i => i >= nIdx); if (index !== -1) { - if (this._currentValueIndices[index] === nIdx) { - this._currentValueIndices.splice(index, 1); + if (this.#currentValueIndices[index] === nIdx) { + this.#currentValueIndices.splice(index, 1); } - for (const ii = this._currentValueIndices.length; index < ii; index++) { - --this._currentValueIndices[index]; + for (const ii = this.#currentValueIndices.length; index < ii; index++) { + --this.#currentValueIndices[index]; } } - } else if (this._currentValueIndices === nIdx) { - this._currentValueIndices = this.numItems > 0 ? 0 : -1; - } else if (this._currentValueIndices > nIdx) { - --this._currentValueIndices; + } else if (this.#currentValueIndices === nIdx) { + this.#currentValueIndices = this.numItems > 0 ? 0 : -1; + } else if (this.#currentValueIndices > nIdx) { + --this.#currentValueIndices; } - this._send({ id: this._id, remove: nIdx }); + this.#send({ id: this.#id, remove: nIdx }); } getItemAt(nIdx = -1, bExportValue = false) { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } if (nIdx < 0 || nIdx >= this.numItems) { nIdx = this.numItems - 1; } - const item = this._items[nIdx]; + const item = this.#items[nIdx]; return bExportValue ? item.exportValue : item.displayValue; } getArray() { // Gets the array of terminal child fields (that is, fields that can have // a value for this Field object, the parent field). - if (this._kidIds) { + if (this.#kidIds) { const array = []; const fillArrayWithKids = kidIds => { for (const id of kidIds) { - const obj = this._appObjects[id]; + const obj = this.#appObjects[id]; if (!obj) { continue; } - if (obj.obj._hasValue) { - array.push(obj.wrapped); + const fp = this.#getFieldPrivate(obj); + if (fp.getHasValue(obj)) { + array.push(obj); } - if (obj.obj._kidIds) { - fillArrayWithKids(obj.obj._kidIds); + const kids = fp.getKidIds(obj); + if (kids) { + fillArrayWithKids(kids); } } }; - fillArrayWithKids(this._kidIds); + fillArrayWithKids(this.#kidIds); return array; } - return (this._children ??= this._document.obj._getTerminalChildren( - this._fieldPath + return (this.#children ??= this.#docInternals.getTerminalChildren( + this.#fieldPath )); } @@ -471,7 +683,7 @@ class Field extends PDFObject { } insertItemAt(cName, cExport = undefined, nIdx = 0) { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } if (!cName) { @@ -482,7 +694,7 @@ class Field extends PDFObject { nIdx = this.numItems; } - if (this._items.some(({ displayValue }) => displayValue === cName)) { + if (this.#items.some(({ displayValue }) => displayValue === cName)) { return; } @@ -490,40 +702,37 @@ class Field extends PDFObject { cExport = cName; } const data = { displayValue: cName, exportValue: cExport }; - this._items.splice(nIdx, 0, data); - if (Array.isArray(this._currentValueIndices)) { - let index = this._currentValueIndices.findIndex(i => i >= nIdx); + this.#items.splice(nIdx, 0, data); + if (Array.isArray(this.#currentValueIndices)) { + let index = this.#currentValueIndices.findIndex(i => i >= nIdx); if (index !== -1) { - for (const ii = this._currentValueIndices.length; index < ii; index++) { - ++this._currentValueIndices[index]; + for (const ii = this.#currentValueIndices.length; index < ii; index++) { + ++this.#currentValueIndices[index]; } } - } else if (this._currentValueIndices >= nIdx) { - ++this._currentValueIndices; + } else if (this.#currentValueIndices >= nIdx) { + ++this.#currentValueIndices; } - this._send({ id: this._id, insert: { index: nIdx, ...data } }); + this.#send({ id: this.#id, insert: { index: nIdx, ...data } }); } setAction(cTrigger, cScript) { if (typeof cTrigger !== "string" || typeof cScript !== "string") { return; } - if (!(cTrigger in this._actions)) { - this._actions[cTrigger] = []; - } - this._actions[cTrigger].push(cScript); + this.#actions.getOrInsertComputed(cTrigger, () => []).push(cScript); } setFocus() { - this._send({ id: this._id, focus: true }); + this.#send({ id: this.#id, focus: true }); } setItems(oArray) { - if (!this._isChoice) { + if (!this.#isChoice) { throw new Error("Not a choice widget"); } - this._items.length = 0; + this.#items.length = 0; for (const element of oArray) { let displayValue, exportValue; if (Array.isArray(element)) { @@ -532,11 +741,11 @@ class Field extends PDFObject { } else { displayValue = exportValue = element?.toString() || ""; } - this._items.push({ displayValue, exportValue }); + this.#items.push({ displayValue, exportValue }); } - this._currentValueIndices = 0; + this.#currentValueIndices = 0; - this._send({ id: this._id, items: this._items }); + this.#send({ id: this.#id, items: this.#items }); } setLock() {} @@ -553,101 +762,149 @@ class Field extends PDFObject { signatureValidate() {} - _isButton() { - return false; - } - - _reset() { - this.value = this.defaultValue; - } - - _runActions(event) { + #runActions(event) { const eventName = event.name; - if (!this._actions.has(eventName)) { + if (!this.#actions.has(eventName)) { return false; } - const actions = this._actions.get(eventName); + const actions = this.#actions.get(eventName); for (const action of actions) { try { // Action evaluation must happen in the global scope - this._globalEval(action); + this.#globalEval(action); } catch (error) { - const serializedError = serializeError(error); - serializedError.value = `Error when executing "${eventName}" for field "${this._id}"\n${serializedError.value}`; - this._send(serializedError); + const id = this.#id; + this.#send({ + toString() { + return `Error when executing "${eventName}" for field "${id}"\n${error.toString()}`; + }, + stack: error.stack, + }); } } return true; } -} -class RadioButtonField extends Field { + // Send data, including siblings when they exist. + #sendData(data) { + const siblings = this.#fp.getSiblings(this); + if (siblings) { + data.siblings = siblings; + } + this.#send(data); + } +}; + +globalThis.RadioButtonField = class RadioButtonField extends globalThis.Field { + #radioIds; + + #radioActions; + + // Tracks instances whose constructor has fully run. Using a static private + // WeakSet avoids accessing instance #private fields before super() returns, + // which would throw in spec-compliant engines (SpiderMonkey/V8). Static + // private fields are class-scoped and invisible to PDF scripts. + static #initialized = new WeakSet(); + + // fieldPrivate passed in via constructor data. + #fp; + constructor(otherButtons, data) { super(data); + const fp = data.fieldPrivate; + this.#fp = fp; + this.exportValues = [this.exportValues]; - this._radioIds = [this._id]; - this._radioActions = [this._actions]; + this.#radioIds = [fp.getId(this)]; + this.#radioActions = [fp.getActions(this)]; for (const radioData of otherButtons) { this.exportValues.push(radioData.exportValues); - this._radioIds.push(radioData.id); - this._radioActions.push(createActionsMap(radioData.actions)); - if (this._value === radioData.exportValues) { - this._id = radioData.id; + this.#radioIds.push(radioData.id); + this.#radioActions.push( + new Map(radioData.actions ? Object.entries(radioData.actions) : null) + ); + if (fp.getValue(this) === radioData.exportValues) { + fp.setId(this, radioData.id); } } - this._hasBeenInitialized = true; - this._value = data.value || ""; + RadioButtonField.#initialized.add(this); + fp.setValue(this, data.value || ""); } - get _siblings() { - return this._radioIds.filter(id => id !== this._id); + static claimInternals(fieldPrivate) { + delete RadioButtonField.claimInternals; + return Object.assign(Object.create(fieldPrivate), { + getRadioIds: f => f.#radioIds, + getSiblings: f => f.#radioIds.filter(id => id !== fieldPrivate.getId(f)), + isButton: () => true, + getExportValue: (f, _state) => { + const i = f.#radioIds.indexOf(fieldPrivate.getId(f)); + return f.exportValues[i]; + }, + runActions: (f, event) => { + const i = f.#radioIds.indexOf(fieldPrivate.getId(f)); + fieldPrivate.setActions(f, f.#radioActions[i]); + return fieldPrivate.runActions(f, event); + }, + }); } - set _siblings(_) {} - get value() { - return this._value; + return this.#fp.getValue(this); } set value(value) { - if (!this._hasBeenInitialized) { + if (!RadioButtonField.#initialized.has(this)) { return; } + const fp = this.#fp; if (value === null || value === undefined) { - this._value = ""; + fp.setValue(this, ""); + return; } const i = this.exportValues.indexOf(value); - if (0 <= i && i < this._radioIds.length) { - this._id = this._radioIds[i]; - this._value = value; - } else if (value === "Off" && this._radioIds.length === 2) { - const nextI = (1 + this._radioIds.indexOf(this._id)) % 2; - this._id = this._radioIds[nextI]; - this._value = this.exportValues[nextI]; + if (0 <= i && i < this.#radioIds.length) { + fp.setId(this, this.#radioIds[i]); + fp.setValue(this, value); + } else if (value === "Off" && this.#radioIds.length === 2) { + const nextI = (1 + this.#radioIds.indexOf(fp.getId(this))) % 2; + fp.setId(this, this.#radioIds[nextI]); + fp.setValue(this, this.exportValues[nextI]); + } else { + return; } + fp.send(this, { + id: fp.getId(this), + siblings: this.#radioIds.filter(id => id !== fp.getId(this)), + value: fp.getValue(this), + }); } checkThisBox(nWidget, bCheckIt = true) { - if (nWidget < 0 || nWidget >= this._radioIds.length || !bCheckIt) { + if (nWidget < 0 || nWidget >= this.#radioIds.length || !bCheckIt) { return; } - this._id = this._radioIds[nWidget]; - this._value = this.exportValues[nWidget]; - this._send({ id: this._id, value: this._value }); + const fp = this.#fp; + fp.setId(this, this.#radioIds[nWidget]); + fp.setValue(this, this.exportValues[nWidget]); + fp.send(this, { + id: fp.getId(this), + value: fp.getValue(this), + }); } isBoxChecked(nWidget) { return ( nWidget >= 0 && - nWidget < this._radioIds.length && - this._id === this._radioIds[nWidget] + nWidget < this.#radioIds.length && + this.#fp.getId(this) === this.#radioIds[nWidget] ); } @@ -658,42 +915,52 @@ class RadioButtonField extends Field { this.defaultValue === this.exportValues[nWidget] ); } +}; - _getExportValue(state) { - const i = this._radioIds.indexOf(this._id); - return this.exportValues[i]; - } +globalThis.CheckboxField = class CheckboxField extends ( + globalThis.RadioButtonField +) { + static #initialized = new WeakSet(); + + // fieldPrivate and radioFieldPrivate passed in via constructor data. + #fp; - _runActions(event) { - const i = this._radioIds.indexOf(this._id); - this._actions = this._radioActions[i]; - return super._runActions(event); + #rfp; + + constructor(otherButtons, data) { + super(otherButtons, data); + this.#fp = data.fieldPrivate; + this.#rfp = data.radioFieldPrivate; + CheckboxField.#initialized.add(this); } - _isButton() { - return true; + static claimInternals(radioFieldPrivate) { + delete CheckboxField.claimInternals; + return Object.assign(Object.create(radioFieldPrivate), { + getExportValue: (f, state) => + state ? radioFieldPrivate.getExportValue(f, state) : "Off", + }); } -} -class CheckboxField extends RadioButtonField { get value() { - return this._value; + return this.#fp.getValue(this); } set value(value) { + if (!CheckboxField.#initialized.has(this)) { + return; + } + const fp = this.#fp; if (!value || value === "Off") { - this._value = "Off"; + fp.setValue(this, "Off"); + fp.send(this, { id: fp.getId(this), value: "Off" }); } else { super.value = value; } } - _getExportValue(state) { - return state ? super._getExportValue(state) : "Off"; - } - isBoxChecked(nWidget) { - if (this._value === "Off") { + if (this.#fp.getValue(this) === "Off") { return false; } return super.isBoxChecked(nWidget); @@ -701,19 +968,24 @@ class CheckboxField extends RadioButtonField { isDefaultChecked(nWidget) { if (this.defaultValue === "Off") { - return this._value === "Off"; + return this.#fp.getValue(this) === "Off"; } return super.isDefaultChecked(nWidget); } checkThisBox(nWidget, bCheckIt = true) { - if (nWidget < 0 || nWidget >= this._radioIds.length) { + const radioIds = this.#rfp.getRadioIds(this); + if (nWidget < 0 || nWidget >= radioIds.length) { return; } - this._id = this._radioIds[nWidget]; - this._value = bCheckIt ? this.exportValues[nWidget] : "Off"; - this._send({ id: this._id, value: this._value }); + const fp = this.#fp; + fp.setId(this, radioIds[nWidget]); + fp.setValue(this, bCheckIt ? this.exportValues[nWidget] : "Off"); + fp.send(this, { + id: fp.getId(this), + value: fp.getValue(this), + }); } -} +}; -export { CheckboxField, Field, RadioButtonField }; +export {}; diff --git a/src/scripting_api/fullscreen.js b/src/scripting_api/fullscreen.js index cd6901a196bcc..bddd0b95814f6 100644 --- a/src/scripting_api/fullscreen.js +++ b/src/scripting_api/fullscreen.js @@ -13,32 +13,31 @@ * limitations under the License. */ -import { Cursor } from "./constants.js"; -import { PDFObject } from "./pdf_object.js"; +import { cursor } from "./constants.js"; -class FullScreen extends PDFObject { - _backgroundColor = []; +globalThis.FullScreen = class FullScreen { + #backgroundColor = []; - _clickAdvances = true; + #clickAdvances = true; - _cursor = Cursor.hidden; + #cursor = cursor.hidden; - _defaultTransition = ""; + #defaultTransition = ""; - _escapeExits = true; + #escapeExits = true; - _isFullScreen = true; + #isFullScreen = true; - _loop = false; + #loop = false; - _timeDelay = 3600; + #timeDelay = 3600; - _usePageTiming = false; + #usePageTiming = false; - _useTimer = false; + #useTimer = false; get backgroundColor() { - return this._backgroundColor; + return this.#backgroundColor; } set backgroundColor(_) { @@ -46,7 +45,7 @@ class FullScreen extends PDFObject { } get clickAdvances() { - return this._clickAdvances; + return this.#clickAdvances; } set clickAdvances(_) { @@ -54,7 +53,7 @@ class FullScreen extends PDFObject { } get cursor() { - return this._cursor; + return this.#cursor; } set cursor(_) { @@ -62,7 +61,7 @@ class FullScreen extends PDFObject { } get defaultTransition() { - return this._defaultTransition; + return this.#defaultTransition; } set defaultTransition(_) { @@ -70,7 +69,7 @@ class FullScreen extends PDFObject { } get escapeExits() { - return this._escapeExits; + return this.#escapeExits; } set escapeExits(_) { @@ -78,7 +77,7 @@ class FullScreen extends PDFObject { } get isFullScreen() { - return this._isFullScreen; + return this.#isFullScreen; } set isFullScreen(_) { @@ -86,7 +85,7 @@ class FullScreen extends PDFObject { } get loop() { - return this._loop; + return this.#loop; } set loop(_) { @@ -94,7 +93,7 @@ class FullScreen extends PDFObject { } get timeDelay() { - return this._timeDelay; + return this.#timeDelay; } set timeDelay(_) { @@ -131,7 +130,7 @@ class FullScreen extends PDFObject { } get usePageTiming() { - return this._usePageTiming; + return this.#usePageTiming; } set usePageTiming(_) { @@ -139,12 +138,12 @@ class FullScreen extends PDFObject { } get useTimer() { - return this._useTimer; + return this.#useTimer; } set useTimer(_) { /* TODO or not */ } -} +}; -export { FullScreen }; +export {}; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index 9e4727bcb1015..81788670eafc9 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -13,207 +13,236 @@ * limitations under the License. */ -import { - Border, - Cursor, - Display, - Font, - GlobalConstants, - Highlight, - Position, - ScaleHow, - ScaleWhen, - Style, - Trans, - ZoomType, -} from "./constants.js"; -import { CheckboxField, Field, RadioButtonField } from "./field.js"; -import { AForm } from "./aform.js"; -import { App } from "./app.js"; -import { Color } from "./color.js"; -import { Console } from "./console.js"; -import { Doc } from "./doc.js"; -import { ProxyHandler } from "./proxy.js"; -import { serializeError } from "./app_utils.js"; -import { Util } from "./util.js"; - -function initSandbox(params) { - delete globalThis.pdfjsScripting; - - // externalCall is a function to call a function defined - // outside the sandbox. - // (see src/pdf.sandbox.external.js). - const externalCall = globalThis.callExternalFunction; - delete globalThis.callExternalFunction; - - // eslint-disable-next-line no-eval - const globalEval = code => globalThis.eval(code); - const send = data => externalCall("send", [data]); - const proxyHandler = new ProxyHandler(); - const { data } = params; - const doc = new Doc({ - send, - globalEval, - ...data.docInfo, - }); - const _document = { obj: doc, wrapped: new Proxy(doc, proxyHandler) }; - const app = new App({ - send, - globalEval, - externalCall, - _document, - calculationOrder: data.calculationOrder, - proxyHandler, - ...data.appInfo, - }); - - const util = new Util({ externalCall }); - const appObjects = app._objects; - - if (data.objects) { - const annotations = []; - - for (const [name, objs] of Object.entries(data.objects)) { - annotations.length = 0; - let container = null; - - for (const obj of objs) { - if (obj.type !== "") { - annotations.push(obj); - } else { - container = obj; - } +import "./app_utils.js"; +import "./app.js"; +import "./aform.js"; +import "./color.js"; +import "./console.js"; +import "./doc.js"; +import "./event.js"; +import "./field.js"; +import "./util.js"; +import { ADBE } from "./constants.js"; + +globalThis.pdfjsScripting = { + initSandbox(params) { + delete globalThis.pdfjsScripting; + + // externalCall is a function to call a function defined + // outside the sandbox. + // (see src/pdf.sandbox.external.js). + const externalCall = globalThis.callExternalFunction; + delete globalThis.callExternalFunction; + + // eslint-disable-next-line no-eval + const globalEval = code => globalThis.eval(code); + const send = data => { + if (data.stack) { + data = { command: "error", value: `${data.toString()}\n${data.stack}` }; } - - let obj = container; - if (annotations.length > 0) { - obj = annotations[0]; - obj.send = send; + externalCall("send", [data]); + }; + const appObjects = Object.create(null); + const { data } = params; + + const { + AForm, + App, + CheckboxField, + Console, + Doc, + EventDispatcher, + Field, + RadioButtonField, + Util, + } = globalThis; + delete globalThis.AForm; + delete globalThis.App; + delete globalThis.CheckboxField; + delete globalThis.Console; + delete globalThis.Doc; + delete globalThis.EventDispatcher; + delete globalThis.Field; + delete globalThis.RadioButtonField; + delete globalThis.Util; + + // Claim all internal factories before any instance is created. + // Each is a one-shot: claimInternals() deletes itself from the class after + // the first call, making it inaccessible from that point on. + const getDocInternals = Doc.claimInternals(); + const getEventDispatcherInternals = EventDispatcher.claimInternals(); + const getAppInternals = App.claimInternals(); + const getUtilInternals = Util.claimInternals(); + const fieldPrivate = Field.claimInternals(); + const radioFieldPrivate = RadioButtonField.claimInternals(fieldPrivate); + const checkboxFieldPrivate = + CheckboxField.claimInternals(radioFieldPrivate); + + function getFieldPrivate(field) { + if (field instanceof CheckboxField) { + return checkboxFieldPrivate; + } + if (field instanceof RadioButtonField) { + return radioFieldPrivate; } + return fieldPrivate; + } - obj.globalEval = globalEval; - obj.doc = _document; - obj.fieldPath = name; - obj.appObjects = appObjects; - obj.util = util; + const userActivationData = { + userActivation: false, + disablePrinting: false, + disableSaving: false, + }; + const doc = new Doc({ + send, + globalEval, + userActivationData, + ...data.docInfo, + getFieldPrivate, + }); + const docInternals = getDocInternals(doc); + + const eventDispatcher = new EventDispatcher( + doc, + data.calculationOrder, + appObjects, + externalCall, + getFieldPrivate, + docInternals, + userActivationData + ); + const eventDispatcherInternals = + getEventDispatcherInternals(eventDispatcher); + + const app = (globalThis.app = new App({ + send, + globalEval, + userActivationData, + externalCall, + doc, + calculationOrder: data.calculationOrder, + ...data.appInfo, + getFieldPrivate, + })); + const appInternals = getAppInternals(app); + + const util = (globalThis.util = new Util()); + const utilInternals = getUtilInternals(util); + + if (data.objects) { + const annotations = []; + + for (const [name, objs] of Object.entries(data.objects)) { + annotations.length = 0; + let container = null; + + for (const obj of objs) { + if (obj.type !== "") { + annotations.push(obj); + } else { + container = obj; + } + } + + let obj = container; + if (annotations.length > 0) { + obj = annotations[0]; + obj.send = send; + } - const otherFields = annotations.slice(1); + obj.globalEval = globalEval; + obj.fieldPath = name; + obj.appObjects = appObjects; + obj.util = util; + obj.fieldPrivate = fieldPrivate; + obj.radioFieldPrivate = radioFieldPrivate; + obj.checkboxFieldPrivate = checkboxFieldPrivate; + obj.getFieldPrivate = getFieldPrivate; + obj.docInternals = docInternals; + + const otherFields = annotations.slice(1); + + let field; + switch (obj.type) { + case "radiobutton": { + obj.fieldPrivate = radioFieldPrivate; + field = new RadioButtonField(otherFields, obj); + break; + } + case "checkbox": { + obj.fieldPrivate = checkboxFieldPrivate; + field = new CheckboxField(otherFields, obj); + break; + } + default: + if (otherFields.length > 0) { + obj.siblings = otherFields.map(x => x.id); + } + field = new Field(obj); + } - let field; - switch (obj.type) { - case "radiobutton": { - field = new RadioButtonField(otherFields, obj); - break; + docInternals.addField(name, field); + for (const object of objs) { + appObjects[object.id] = field; } - case "checkbox": { - field = new CheckboxField(otherFields, obj); - break; + if (container) { + appObjects[container.id] = field; } - default: - if (otherFields.length > 0) { - obj.siblings = otherFields.map(x => x.id); - } - field = new Field(obj); } + } - const wrapped = new Proxy(field, proxyHandler); - const _object = { obj: field, wrapped }; - doc._addField(name, _object); - for (const object of objs) { - appObjects[object.id] = _object; + globalThis.event = null; + globalThis.global = Object.create(null); + globalThis.console = new Console({ send }); + globalThis.ADBE = ADBE; + + // AF... functions + const aform = new AForm( + doc, + app, + util, + utilInternals, + eventDispatcherInternals.mergeChange + ); + for (const name of Object.getOwnPropertyNames(AForm.prototype)) { + if (name !== "constructor") { + globalThis[name] = aform[name].bind(aform); } - if (container) { - appObjects[container.id] = _object; - } - } - } - - const color = new Color(); - - globalThis.event = null; - globalThis.global = Object.create(null); - globalThis.app = new Proxy(app, proxyHandler); - globalThis.color = new Proxy(color, proxyHandler); - globalThis.console = new Proxy(new Console({ send }), proxyHandler); - globalThis.util = new Proxy(util, proxyHandler); - globalThis.border = Border; - globalThis.cursor = Cursor; - globalThis.display = Display; - globalThis.font = Font; - globalThis.highlight = Highlight; - globalThis.position = Position; - globalThis.scaleHow = ScaleHow; - globalThis.scaleWhen = ScaleWhen; - globalThis.style = Style; - globalThis.trans = Trans; - globalThis.zoomtype = ZoomType; - - // Avoid to have a popup asking to update Acrobat. - globalThis.ADBE = { - Reader_Value_Asked: true, - Viewer_Value_Asked: true, - }; - - // AF... functions - const aform = new AForm(doc, app, util, color); - for (const name of Object.getOwnPropertyNames(AForm.prototype)) { - if (name !== "constructor" && !name.startsWith("_")) { - globalThis[name] = aform[name].bind(aform); } - } - // Add global constants such as IDS_GREATER_THAN or RE_NUMBER_ENTRY_DOT_SEP - for (const [name, value] of Object.entries(GlobalConstants)) { - Object.defineProperty(globalThis, name, { - value, - writable: false, - }); - } - - // Color functions - Object.defineProperties(globalThis, { - ColorConvert: { - value: color.convert.bind(color), - writable: true, - }, - ColorEqual: { - value: color.equal.bind(color), - writable: true, - }, - }); - - // 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), - }; - - return (name, args) => { - try { - functions[name](args); - } catch (error) { - send(serializeError(error)); + // 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: eventDispatcherInternals.dispatch, + timeoutCb: appInternals.evalCallback, + }; + + return (name, args) => { + try { + functions[name](args); + } catch (error) { + send(error); + } + }; + }, +}; -export { initSandbox }; +export {}; diff --git a/src/scripting_api/pdf_object.js b/src/scripting_api/pdf_object.js deleted file mode 100644 index dba285a2535d1..0000000000000 --- a/src/scripting_api/pdf_object.js +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2020 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -class PDFObject { - constructor(data) { - this._expandos = Object.create(null); - this._send = data.send || null; - this._id = data.id || null; - } -} - -export { PDFObject }; diff --git a/src/scripting_api/print_params.js b/src/scripting_api/print_params.js index 08de9e1f53dd9..09e35e35d1122 100644 --- a/src/scripting_api/print_params.js +++ b/src/scripting_api/print_params.js @@ -13,7 +13,7 @@ * limitations under the License. */ -class PrintParams { +globalThis.PrintParams = class PrintParams { binaryOk = true; bitmapDPI = 150; @@ -174,6 +174,6 @@ class PrintParams { constructor(data) { this.lastPage = data.lastPage; } -} +}; -export { PrintParams }; +export {}; diff --git a/src/scripting_api/proxy.js b/src/scripting_api/proxy.js deleted file mode 100644 index d8bb6f0cbdcf1..0000000000000 --- a/src/scripting_api/proxy.js +++ /dev/null @@ -1,137 +0,0 @@ -/* Copyright 2020 Mozilla Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -class ProxyHandler { - // Don't dispatch an event for those properties. - // - delay: allow to delay field redraw until delay is set to false. - // Likely it's useless to implement that stuff. - nosend = new Set(["delay"]); - - get(obj, prop) { - // script may add some properties to the object - if (prop in obj._expandos) { - const val = obj._expandos[prop]; - if (typeof val === "function") { - return val.bind(obj); - } - return val; - } - - if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { - // return only public properties - // i.e. the ones not starting with a '_' - const val = obj[prop]; - if (typeof val === "function") { - return val.bind(obj); - } - return val; - } - - return undefined; - } - - set(obj, prop, value) { - if (obj._kidIds) { - // If the field is a container for other fields then - // dispatch the kids. - obj._kidIds.forEach(id => { - obj._appObjects[id].wrapped[prop] = value; - }); - } - - if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { - const old = obj[prop]; - obj[prop] = value; - if ( - !this.nosend.has(prop) && - obj._send && - obj._id !== null && - typeof old !== "function" - ) { - const data = { id: obj._id }; - data[prop] = prop === "value" ? obj._getValue() : obj[prop]; - - // send the updated value to the other side - if (!obj._siblings) { - obj._send(data); - } else { - data.siblings = obj._siblings; - obj._send(data); - } - } - } else { - obj._expandos[prop] = value; - } - return true; - } - - has(obj, prop) { - return ( - prop in obj._expandos || - (typeof prop === "string" && !prop.startsWith("_") && prop in obj) - ); - } - - getPrototypeOf(obj) { - return null; - } - - setPrototypeOf(obj, proto) { - return false; - } - - isExtensible(obj) { - return true; - } - - preventExtensions(obj) { - return false; - } - - getOwnPropertyDescriptor(obj, prop) { - if (prop in obj._expandos) { - return { - configurable: true, - enumerable: true, - value: obj._expandos[prop], - }; - } - - if (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { - return { configurable: true, enumerable: true, value: obj[prop] }; - } - - return undefined; - } - - defineProperty(obj, key, descriptor) { - Object.defineProperty(obj._expandos, key, descriptor); - return true; - } - - deleteProperty(obj, prop) { - if (prop in obj._expandos) { - delete obj._expandos[prop]; - } - } - - ownKeys(obj) { - const fromExpandos = Reflect.ownKeys(obj._expandos); - const fromObj = Reflect.ownKeys(obj).filter(k => !k.startsWith("_")); - return fromExpandos.concat(fromObj); - } -} - -export { ProxyHandler }; diff --git a/src/scripting_api/thermometer.js b/src/scripting_api/thermometer.js index ef5cd2205ebec..34b44e1e2c470 100644 --- a/src/scripting_api/thermometer.js +++ b/src/scripting_api/thermometer.js @@ -13,19 +13,17 @@ * limitations under the License. */ -import { PDFObject } from "./pdf_object.js"; +globalThis.Thermometer = class Thermometer { + #cancelled = false; -class Thermometer extends PDFObject { - _cancelled = false; + #duration = 100; - _duration = 100; + #text = ""; - _text = ""; - - _value = 0; + #value = 0; get cancelled() { - return this._cancelled; + return this.#cancelled; } set cancelled(_) { @@ -33,27 +31,27 @@ class Thermometer extends PDFObject { } get duration() { - return this._duration; + return this.#duration; } set duration(val) { - this._duration = val; + this.#duration = val; } get text() { - return this._text; + return this.#text; } set text(val) { - this._text = val; + this.#text = val; } get value() { - return this._value; + return this.#value; } set value(val) { - this._value = val; + this.#value = val; } begin() { @@ -63,6 +61,6 @@ class Thermometer extends PDFObject { end() { /* TODO */ } -} +}; -export { Thermometer }; +export {}; diff --git a/src/scripting_api/util.js b/src/scripting_api/util.js index 91229b0d73d19..376475f07fcfa 100644 --- a/src/scripting_api/util.js +++ b/src/scripting_api/util.js @@ -13,16 +13,29 @@ * limitations under the License. */ -import { PDFObject } from "./pdf_object.js"; +globalThis.Util = class Util { + static claimInternals() { + delete Util.claimInternals; + return instance => ({ + scand: instance.#scand.bind(instance), + }); + } -class Util extends PDFObject { #dateActionsCache = null; - constructor(data) { - super(data); + #scandCache; + + #months; + + #days; - this._scandCache = new Map(); - this._months = [ + MILLISECONDS_IN_DAY = 86400000; + + MILLISECONDS_IN_WEEK = 604800000; + + constructor() { + this.#scandCache = new Map(); + this.#months = [ "January", "February", "March", @@ -36,7 +49,7 @@ class Util extends PDFObject { "November", "December", ]; - this._days = [ + this.#days = [ "Sunday", "Monday", "Tuesday", @@ -45,11 +58,6 @@ class Util extends PDFObject { "Friday", "Saturday", ]; - this.MILLISECONDS_IN_DAY = 86400000; - this.MILLISECONDS_IN_WEEK = 604800000; - - // used with crackURL - this._externalCall = data.externalCall; } printf(...args) { @@ -219,12 +227,12 @@ class Util extends PDFObject { } const handlers = { - mmmm: data => this._months[data.month], - mmm: data => this._months[data.month].substring(0, 3), + mmmm: data => this.#months[data.month], + mmm: data => this.#months[data.month].substring(0, 3), mm: data => (data.month + 1).toString().padStart(2, "0"), m: data => (data.month + 1).toString(), - dddd: data => this._days[data.dayOfWeek], - ddd: data => this._days[data.dayOfWeek].substring(0, 3), + dddd: data => this.#days[data.dayOfWeek], + ddd: data => this.#days[data.dayOfWeek].substring(0, 3), dd: data => data.day.toString().padStart(2, "0"), d: data => data.day.toString(), yyyy: data => data.year.toString().padStart(4, "0"), @@ -442,10 +450,11 @@ class Util extends PDFObject { } scand(cFormat, cDate) { - return this._scand(cFormat, cDate); + return this.#scand(cFormat, cDate); } - _scand(cFormat, cDate, strict = false) { + // Keep _scand accessible from aform.js (trusted code). + #scand(cFormat, cDate, strict = false) { if (typeof cDate !== "string") { return new Date(cDate); } @@ -463,9 +472,9 @@ class Util extends PDFObject { return this.scand("m/d/yy h:MM:ss tt", cDate); } - if (!this._scandCache.has(cFormat)) { - const months = this._months; - const days = this._days; + if (!this.#scandCache.has(cFormat)) { + const months = this.#months; + const days = this.#days; const handlers = { mmmm: { @@ -608,10 +617,10 @@ class Util extends PDFObject { } ); - this._scandCache.set(cFormat, [re, actions]); + this.#scandCache.set(cFormat, [re, actions]); } - const [re, actions] = this._scandCache.get(cFormat); + const [re, actions] = this.#scandCache.get(cFormat); const matches = new RegExp(`^${re}$`, "g").exec(cDate); if (!matches || matches.length !== actions.length + 1) { @@ -653,6 +662,6 @@ class Util extends PDFObject { xmlToSpans() { /* Not implemented */ } -} +}; -export { Util }; +export {}; diff --git a/src/shared/scripting_utils.js b/src/shared/scripting_utils.js index e508534f4d4ba..1dd2182e913c8 100644 --- a/src/shared/scripting_utils.js +++ b/src/shared/scripting_utils.js @@ -22,18 +22,18 @@ import { MathClamp } from "../shared/math_clamp.js"; -function makeColorComp(n) { - return Math.floor(MathClamp(n, 0, 1) * 255) - .toString(16) - .padStart(2, "0"); -} - -function scaleAndClamp(x) { - return MathClamp(x, 0, 1) * 255; -} - // PDF specifications section 10.3 class ColorConverters { + static #makeColorComp(n) { + return Math.floor(MathClamp(n, 0, 1) * 255) + .toString(16) + .padStart(2, "0"); + } + + static #scaleAndClamp(x) { + return MathClamp(x, 0, 1) * 255; + } + static CMYK_G([c, y, m, k]) { return ["G", 1 - Math.min(1, 0.3 * c + 0.59 * m + 0.11 * y + k)]; } @@ -47,12 +47,12 @@ class ColorConverters { } static G_rgb([g]) { - g = scaleAndClamp(g); + g = this.#scaleAndClamp(g); return [g, g, g]; } static G_HTML([g]) { - const G = makeColorComp(g); + const G = this.#makeColorComp(g); return `#${G}${G}${G}`; } @@ -61,11 +61,11 @@ class ColorConverters { } static RGB_rgb(color) { - return color.map(scaleAndClamp); + return color.map(this.#scaleAndClamp.bind(this)); } static RGB_HTML(color) { - return `#${color.map(makeColorComp).join("")}`; + return `#${color.map(this.#makeColorComp.bind(this)).join("")}`; } static T_HTML() { @@ -87,9 +87,9 @@ class ColorConverters { static CMYK_rgb([c, y, m, k]) { return [ - scaleAndClamp(1 - Math.min(1, c + k)), - scaleAndClamp(1 - Math.min(1, m + k)), - scaleAndClamp(1 - Math.min(1, y + k)), + this.#scaleAndClamp(1 - Math.min(1, c + k)), + this.#scaleAndClamp(1 - Math.min(1, m + k)), + this.#scaleAndClamp(1 - Math.min(1, y + k)), ]; }