diff --git a/extensions/chromium/preferences_schema.json b/extensions/chromium/preferences_schema.json index 149a4fcd85172..0723e6aede6c1 100644 --- a/extensions/chromium/preferences_schema.json +++ b/extensions/chromium/preferences_schema.json @@ -151,6 +151,10 @@ "type": "boolean", "default": true }, + "enableScripting": { + "type": "boolean", + "default": false + }, "enablePermissions": { "type": "boolean", "default": false diff --git a/gulpfile.js b/gulpfile.js index 07ff79380a7ef..d6b79b0c72499 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -316,6 +316,23 @@ function createMainBundle(defines) { .pipe(replaceJSRootName(mainAMDName, "pdfjsLib")); } +function createScriptingBundle(defines) { + var mainAMDName = "pdfjs-dist/build/pdf.scripting"; + var mainOutputName = "pdf.scripting.js"; + + var mainFileConfig = createWebpackConfig(defines, { + filename: mainOutputName, + library: mainAMDName, + libraryTarget: "umd", + umdNamedDefine: true, + }); + return gulp + .src("./src/scripting_api/initialization.js") + .pipe(webpack2Stream(mainFileConfig)) + .pipe(replaceWebpackRequire()) + .pipe(replaceJSRootName(mainAMDName, "pdfjsScripting")); +} + function createWorkerBundle(defines) { var workerAMDName = "pdfjs-dist/build/pdf.worker"; var workerOutputName = "pdf.worker.js"; @@ -1036,6 +1053,9 @@ gulp.task( createMainBundle(defines).pipe( gulp.dest(MOZCENTRAL_CONTENT_DIR + "build") ), + createScriptingBundle(defines).pipe( + gulp.dest(MOZCENTRAL_CONTENT_DIR + "build") + ), createWorkerBundle(defines).pipe( gulp.dest(MOZCENTRAL_CONTENT_DIR + "build") ), diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 8985cbd6f8844..7b96c316bb5f2 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -468,6 +468,8 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { element.setAttribute("value", textContent); } + element.setAttribute("id", id); + element.addEventListener("input", function (event) { storage.setValue(id, event.target.value); }); @@ -476,6 +478,35 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { event.target.setSelectionRange(0, 0); }); + if (this.data.actions) { + element.addEventListener("updateFromSandbox", function (event) { + const data = event.detail; + if ("value" in data) { + event.target.value = event.detail.value; + } else if ("focus" in data) { + event.target.focus({ preventScroll: false }); + } + }); + + for (const eventType of Object.keys(this.data.actions)) { + switch (eventType) { + case "Format": + element.addEventListener("blur", function (event) { + window.dispatchEvent( + new CustomEvent("dispatchEventInSandbox", { + detail: { + id, + name: "Format", + value: event.target.value, + }, + }) + ); + }); + break; + } + } + } + element.disabled = this.data.readOnly; element.name = this.data.fieldName; diff --git a/src/scripting_api/aform.js b/src/scripting_api/aform.js new file mode 100644 index 0000000000000..618f17f627b11 --- /dev/null +++ b/src/scripting_api/aform.js @@ -0,0 +1,46 @@ +/* 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 AForm { + constructor(document, app, util) { + this._document = document; + this._app = app; + this._util = util; + } + + AFNumber_Format( + nDec, + sepStyle, + negStyle, + currStyle, + strCurrency, + bCurrencyPrepend + ) { + const event = this._document._event; + if (!event.value) { + return; + } + + nDec = Math.abs(nDec); + const value = event.value.trim().replace(",", "."); + let number = Number.parseFloat(value); + if (isNaN(number) || !isFinite(number)) { + number = 0; + } + event.value = number.toFixed(nDec); + } +} + +export { AForm }; diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js new file mode 100644 index 0000000000000..2a823b168994e --- /dev/null +++ b/src/scripting_api/app.js @@ -0,0 +1,61 @@ +/* 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. + */ + +import { EventDispatcher } from "./event.js"; +import { NotSupportedError } from "./error.js"; +import { PDFObject } from "./pdf_object.js"; + +class App extends PDFObject { + constructor(data) { + super(data); + this._document = data._document; + this._objects = Object.create(null); + this._eventDispatcher = new EventDispatcher( + this._document, + data.calculationOrder, + this._objects + ); + + // used in proxy.js to check that this the object with the backdoor + this._isApp = true; + } + + // This function is called thanks to the proxy + // when we call app['random_string'] to dispatch the event. + _dispatchEvent(pdfEvent) { + this._eventDispatcher.dispatch(pdfEvent); + } + + get activeDocs() { + return [this._document.wrapped]; + } + + set activeDocs(_) { + throw new NotSupportedError("app.activeDocs"); + } + + alert( + cMsg, + nIcon = 0, + nType = 0, + cTitle = "PDF.js", + oDoc = null, + oCheckbox = null + ) { + this._send({ command: "alert", value: cMsg }); + } +} + +export { App }; diff --git a/src/scripting_api/console.js b/src/scripting_api/console.js new file mode 100644 index 0000000000000..a32550276c29e --- /dev/null +++ b/src/scripting_api/console.js @@ -0,0 +1,38 @@ +/* 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. + */ + +import { PDFObject } from "./pdf_object.js"; + +class Console extends PDFObject { + clear() { + this._send({ id: "clear" }); + } + + hide() { + /* Not implemented */ + } + + println(msg) { + if (typeof msg === "string") { + this._send({ command: "println", value: "PDF.js Console:: " + msg }); + } + } + + show() { + /* Not implemented */ + } +} + +export { Console }; diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js new file mode 100644 index 0000000000000..3eff51931e266 --- /dev/null +++ b/src/scripting_api/doc.js @@ -0,0 +1,48 @@ +/* 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. + */ + +import { PDFObject } from "./pdf_object.js"; + +class Doc extends PDFObject { + constructor(data) { + super(data); + + this._printParams = null; + this._fields = Object.create(null); + this._event = null; + } + + calculateNow() { + this._eventDispatcher.calculateNow(); + } + + getField(cName) { + if (typeof cName !== "string") { + throw new TypeError("Invalid field name: must be a string"); + } + if (cName in this._fields) { + return this._fields[cName]; + } + for (const [name, field] of Object.entries(this._fields)) { + if (name.includes(cName)) { + return field; + } + } + + return undefined; + } +} + +export { Doc }; diff --git a/src/scripting_api/error.js b/src/scripting_api/error.js new file mode 100644 index 0000000000000..4345999162048 --- /dev/null +++ b/src/scripting_api/error.js @@ -0,0 +1,23 @@ +/* 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 new file mode 100644 index 0000000000000..a919fdd91aa7a --- /dev/null +++ b/src/scripting_api/event.js @@ -0,0 +1,79 @@ +/* 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 Event { + constructor(data) { + this.change = data.change || ""; + this.changeEx = data.changeEx || null; + this.commitKey = data.commitKey || 0; + this.fieldFull = data.fieldFull || false; + this.keyDown = data.keyDown || false; + this.modifier = data.modifier || false; + this.name = data.name; + this.rc = true; + this.richChange = data.richChange || []; + this.richChangeEx = data.richChangeEx || []; + this.richValue = data.richValue || []; + this.selEnd = data.selEnd || 0; + this.selStart = data.selStart || 0; + this.shift = data.shift || false; + this.source = data.source || null; + this.target = data.target || null; + this.targetName = data.targetName || ""; + this.type = "Field"; + this.value = data.value || null; + this.willCommit = data.willCommit || false; + } +} + +class EventDispatcher { + constructor(document, calculationOrder, objects) { + this._document = document; + this._calculationOrder = calculationOrder; + this._objects = objects; + + this._document.obj._eventDispatcher = this; + } + + dispatch(baseEvent) { + const id = baseEvent.id; + if (!(id in this._objects)) { + return; + } + + const name = baseEvent.name.replace(" ", ""); + const source = this._objects[id]; + const event = (this._document.obj._event = new Event(baseEvent)); + const oldValue = source.obj.value; + + this.runActions(source, source, event, name); + if (event.rc && oldValue !== event.value) { + source.wrapped.value = event.value; + } + } + + runActions(source, target, event, eventName) { + event.source = source.wrapped; + event.target = target.wrapped; + event.name = eventName; + event.rc = true; + if (!target.obj._runActions(event)) { + return true; + } + return event.rc; + } +} + +export { Event, EventDispatcher }; diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js new file mode 100644 index 0000000000000..d62419530fef9 --- /dev/null +++ b/src/scripting_api/field.js @@ -0,0 +1,123 @@ +/* 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. + */ + +import { PDFObject } from "./pdf_object.js"; + +class Field extends PDFObject { + constructor(data) { + super(data); + this.alignment = data.alignment || "left"; + this.borderStyle = data.borderStyle || ""; + this.buttonAlignX = data.buttonAlignX || 50; + this.buttonAlignY = data.buttonAlignY || 50; + this.buttonFitBounds = data.buttonFitBounds; + this.buttonPosition = data.buttonPosition; + this.buttonScaleHow = data.buttonScaleHow; + this.ButtonScaleWhen = data.buttonScaleWhen; + this.calcOrderIndex = data.calcOrderIndex; + this.charLimit = data.charLimit; + this.comb = data.comb; + this.commitOnSelChange = data.commitOnSelChange; + this.currentValueIndices = data.currentValueIndices; + this.defaultStyle = data.defaultStyle; + this.defaultValue = data.defaultValue; + this.doNotScroll = data.doNotScroll; + this.doNotSpellCheck = data.doNotSpellCheck; + this.delay = data.delay; + this.display = data.display; + this.doc = data.doc; + this.editable = data.editable; + this.exportValues = data.exportValues; + this.fileSelect = data.fileSelect; + this.fillColor = data.fillColor; + 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.numItems = data.numItems; + this.page = data.page; + 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.rotation = data.rotation; + this.strokeColor = data.strokeColor; + this.style = data.style; + this.submitName = data.submitName; + this.textColor = data.textColor; + this.textFont = data.textFont; + this.textSize = data.textSize; + this.type = data.type; + this.userName = data.userName; + this.value = data.value || ""; + this.valueAsString = data.valueAsString; + + // Private + this._actions = Object.create(null); + const doc = (this._document = data.doc); + for (const [eventType, actions] of Object.entries(data.actions)) { + // This code is running in a sandbox so it's safe to use Function + this._actions[eventType] = actions.map(action => + // eslint-disable-next-line no-new-func + Function("event", `with (this) {${action}}`).bind(doc) + ); + } + } + + setAction(cTrigger, cScript) { + if (typeof cTrigger !== "string" || typeof cScript !== "string") { + return; + } + if (!(cTrigger in this._actions)) { + this._actions[cTrigger] = []; + } + this._actions[cTrigger].push(cScript); + } + + setFocus() { + this._send({ id: this._id, focus: true }); + } + + _runActions(event) { + const eventName = event.name; + if (!(eventName in this._actions)) { + return false; + } + + const actions = this._actions[eventName]; + try { + for (const action of actions) { + action(event); + } + } catch (error) { + event.rc = false; + const value = + `\"${error.toString()}\" for event ` + + `\"${eventName}\" in object ${this._id}.` + + `\n${error.stack}`; + this._send({ command: "error", value }); + } + + return true; + } +} + +export { Field }; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js new file mode 100644 index 0000000000000..cf44d4058ce62 --- /dev/null +++ b/src/scripting_api/initialization.js @@ -0,0 +1,57 @@ +/* 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. + */ + +import { AForm } from "./aform.js"; +import { App } from "./app.js"; +import { Console } from "./console.js"; +import { Doc } from "./doc.js"; +import { Field } from "./field.js"; +import { ProxyHandler } from "./proxy.js"; +import { Util } from "./util.js"; + +function initSandbox(data, extra, out) { + const proxyHandler = new ProxyHandler(data.dispatchEventName); + const { send, crackURL } = extra; + const doc = new Doc({ send }); + const _document = { obj: doc, wrapped: new Proxy(doc, proxyHandler) }; + const app = new App({ + send, + _document, + calculationOrder: data.calculationOrder, + }); + const util = new Util({ crackURL }); + const aform = new AForm(doc, app, util); + + for (const [name, objs] of Object.entries(data.objects)) { + const obj = objs[0]; + obj.send = send; + obj.doc = _document.wrapped; + const field = new Field(obj); + const wrapped = (doc._fields[name] = new Proxy(field, proxyHandler)); + app._objects[obj.id] = { obj: field, wrapped }; + } + + out.global = Object.create(null); + out.app = new Proxy(app, proxyHandler); + out.console = new Proxy(new Console({ send }), proxyHandler); + out.util = new Proxy(util, proxyHandler); + for (const name of Object.getOwnPropertyNames(AForm.prototype)) { + if (name.startsWith("AF")) { + out[name] = aform[name].bind(aform); + } + } +} + +export { initSandbox }; diff --git a/src/scripting_api/pdf_object.js b/src/scripting_api/pdf_object.js new file mode 100644 index 0000000000000..dba285a2535d1 --- /dev/null +++ b/src/scripting_api/pdf_object.js @@ -0,0 +1,24 @@ +/* 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/proxy.js b/src/scripting_api/proxy.js new file mode 100644 index 0000000000000..4c10d334b6879 --- /dev/null +++ b/src/scripting_api/proxy.js @@ -0,0 +1,125 @@ +/* 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 { + constructor(dispatchEventName) { + this.dispatchEventName = dispatchEventName; + } + + get(obj, prop) { + if (obj._isApp && prop === this.dispatchEventName) { + // a backdoor to be able to call _dispatchEvent method + // the value of 'dispatchEvent' is generated randomly + // and injected in the code + return obj._dispatchEvent.bind(obj); + } + + // 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 (typeof prop === "string" && !prop.startsWith("_") && prop in obj) { + const old = obj[prop]; + obj[prop] = value; + if (obj._send && obj._id !== null && typeof old !== "function") { + const data = { id: obj._id }; + data[prop] = value; + + // send the updated value to the other side + 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/util.js b/src/scripting_api/util.js new file mode 100644 index 0000000000000..db85f1b2315d7 --- /dev/null +++ b/src/scripting_api/util.js @@ -0,0 +1,30 @@ +/* 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. + */ + +import { PDFObject } from "./pdf_object.js"; + +class Util extends PDFObject { + constructor(data) { + super(data); + + this._crackURL = data.crackURL; + } + + crackURL(cURL) { + return this._crackURL(cURL); + } +} + +export { Util }; diff --git a/web/app.js b/web/app.js index 043cad8176937..95fe841a0e4b4 100644 --- a/web/app.js +++ b/web/app.js @@ -19,6 +19,7 @@ import { AutoPrintRegExp, DEFAULT_SCALE_VALUE, EventBus, + generateRandomStringForSandbox, getActiveOrFocusedElement, getPDFFileNameFromURL, isValidRotation, @@ -179,6 +180,10 @@ class DefaultExternalServices { static get isInAutomation() { return shadow(this, "isInAutomation", false); } + + static get scripting() { + throw new Error("Not implemented: scripting"); + } } const PDFViewerApplication = { @@ -1333,6 +1338,60 @@ const PDFViewerApplication = { this._initializePageLabels(pdfDocument); this._initializeMetadata(pdfDocument); + this._initializeJavaScript(pdfDocument); + }, + + /** + * @private + */ + async _initializeJavaScript(pdfDocument) { + if (!AppOptions.get("enableScripting")) { + return; + } + const objects = await pdfDocument.getFieldObjects(); + const scripting = this.externalServices.scripting; + + window.addEventListener("updateFromSandbox", function (event) { + const detail = event.detail; + const id = detail.id; + if (!id) { + switch (detail.command) { + case "println": + console.log(detail.value); + break; + case "clear": + console.clear(); + break; + case "alert": + // eslint-disable-next-line no-alert + window.alert(detail.value); + break; + case "error": + console.error(detail.value); + break; + } + return; + } + + const element = document.getElementById(id); + if (element) { + element.dispatchEvent(new CustomEvent("updateFromSandbox", { detail })); + } else { + const value = detail.value; + if (value !== undefined && value !== null) { + // the element hasn't been rendered yet so use annotation storage + pdfDocument.annotationStorage.setValue(id, detail.value); + } + } + }); + + window.addEventListener("dispatchEventInSandbox", function (event) { + scripting.dispatchEventInSandbox(event.detail); + }); + + const dispatchEventName = generateRandomStringForSandbox(objects); + const calculationOrder = []; + scripting.createSandbox({ objects, dispatchEventName, calculationOrder }); }, /** diff --git a/web/app_options.js b/web/app_options.js index ec8f628e98eb2..2e04ed0a41fb8 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -65,6 +65,11 @@ const defaultOptions = { value: false, kind: OptionKind.VIEWER + OptionKind.PREFERENCE, }, + enableScripting: { + /** @type {boolean} */ + value: false, + kind: OptionKind.VIEWER + OptionKind.PREFERENCE, + }, enableWebGL: { /** @type {boolean} */ value: false, diff --git a/web/firefoxcom.js b/web/firefoxcom.js index 2e5445b2cc1a4..7c3fd5ae7b0d3 100644 --- a/web/firefoxcom.js +++ b/web/firefoxcom.js @@ -254,6 +254,18 @@ class FirefoxComDataRangeTransport extends PDFDataRangeTransport { } } +const FirefoxScripting = { + createSandbox(data) { + FirefoxCom.requestSync("createSandbox", data); + }, + dispatchEventInSandbox(event, sandboxID) { + FirefoxCom.requestSync("dispatchEventInSandbox", event); + }, + destroySandbox() { + FirefoxCom.requestSync("destroySandbox", null); + }, +}; + class FirefoxExternalServices extends DefaultExternalServices { static updateFindControlState(data) { FirefoxCom.request("updateFindControlState", data); @@ -346,6 +358,10 @@ class FirefoxExternalServices extends DefaultExternalServices { return new MozL10n(mozL10n); } + static get scripting() { + return FirefoxScripting; + } + static get supportsIntegratedFind() { const support = FirefoxCom.requestSync("supportsIntegratedFind"); return shadow(this, "supportsIntegratedFind", support); diff --git a/web/ui_utils.js b/web/ui_utils.js index 6faa50b8e508e..19ea882354459 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -1007,6 +1007,39 @@ function getActiveOrFocusedElement() { return curActiveOrFocused; } +/** + * Generate a random string which is not define somewhere in actions. + * + * @param {WaitOnEventOrTimeoutParameters} + * @returns {Promise} A promise that is resolved with a {WaitOnType} value. + */ +function generateRandomStringForSandbox(objects) { + const allObjects = Object.values(objects).flat(2); + const actions = allObjects.map(obj => Object.values(obj.actions)).flat(2); + + while (true) { + const name = new Uint8Array(64); + if (typeof crypto !== "undefined") { + crypto.getRandomValues(name); + } else { + for (let i = 0, ii = name.length; i < ii; i++) { + name[i] = Math.floor(256 * Math.random()); + } + } + + const nameString = + "_" + + btoa( + Array.from(name) + .map(x => String.fromCharCode(x)) + .join("") + ); + if (actions.every(action => !action.includes(nameString))) { + return nameString; + } + } +} + export { AutoPrintRegExp, CSS_UNITS, @@ -1030,6 +1063,7 @@ export { NullL10n, EventBus, ProgressBar, + generateRandomStringForSandbox, getPDFFileNameFromURL, noContextMenuHandler, parseQueryString,