From 10d19652cad15d5191d718752f289a278f170bff Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 28 Oct 2020 19:16:56 +0100 Subject: [PATCH] JS -- Add listener for sandbox events only if there are some actions * When no actions then set it to null instead of empty object * Even if a field has no actions, it needs to listen to events from the sandbox in order to be updated if an action changes something in it. --- src/core/annotation.js | 22 +++++++++++-- src/core/document.js | 24 +++++++++++++- src/core/worker.js | 4 +++ src/display/annotation_layer.js | 36 +++++++++++---------- src/display/api.js | 12 +++++++ src/scripting_api/field.js | 14 +++++---- test/unit/annotation_spec.js | 4 +++ test/unit/document_spec.js | 55 +++++++++++++++++++++++++++++++++ web/annotation_layer_builder.js | 9 +++++- web/app.js | 1 + web/base_viewer.js | 8 ++++- web/interfaces.js | 4 ++- web/pdf_page_view.js | 5 ++- web/ui_utils.js | 5 ++- 14 files changed, 172 insertions(+), 31 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index 3241152d768828..76920526dc7125 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -59,17 +59,24 @@ class AnnotationFactory { * @param {Object} ref * @param {PDFManager} pdfManager * @param {Object} idFactory + * @param {boolean} checkJSActions * @returns {Promise} A promise that is resolved with an {Annotation} * instance. */ - static create(xref, ref, pdfManager, idFactory) { - return pdfManager.ensureCatalog("acroForm").then(acroForm => { + static create(xref, ref, pdfManager, idFactory, checkJSActions = true) { + return Promise.all([ + pdfManager.ensureCatalog("acroForm"), + checkJSActions + ? pdfManager.ensureDoc("hasJSActions") + : Promise.resolve(false), + ]).then(([acroForm, hasJSActions]) => { return pdfManager.ensure(this, "_create", [ xref, ref, pdfManager, idFactory, acroForm, + hasJSActions, ]); }); } @@ -77,7 +84,14 @@ class AnnotationFactory { /** * @private */ - static _create(xref, ref, pdfManager, idFactory, acroForm) { + static _create( + xref, + ref, + pdfManager, + idFactory, + acroForm, + hasJSActions = false + ) { const dict = xref.fetchIfRef(ref); if (!isDict(dict)) { return undefined; @@ -98,6 +112,7 @@ class AnnotationFactory { id, pdfManager, acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, + hasJSActions, }; switch (subtype) { @@ -282,6 +297,7 @@ class Annotation { modificationDate: this.modificationDate, rect: this.rectangle, subtype: params.subtype, + documentHasJSActions: params.hasJSActions, }; this._fallbackFontDict = null; diff --git a/src/core/document.js b/src/core/document.js index cb9e474d3bd21c..bf713cd2ca1086 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -962,12 +962,14 @@ class PDFDocument { if (!(name in promises)) { promises.set(name, []); } + promises.get(name).push( AnnotationFactory.create( this.xref, fieldRef, this.pdfManager, - this._localIdFactory + this._localIdFactory, + /* checkJSActions */ false ) .then(annotation => annotation && annotation.getFieldObject()) .catch(function (reason) { @@ -1014,6 +1016,26 @@ class PDFDocument { ); } + get hasJSActions() { + return shadow( + this, + "hasJSActions", + this.fieldObjects.then(objects => { + if (objects === null) { + return false; + } + for (const object of Object.values(objects)) { + for (const obj of object) { + if (obj.actions !== null) { + return true; + } + } + } + return false; + }) + ); + } + get calculationOrderIds() { const acroForm = this.catalog.acroForm; if (!acroForm || !acroForm.has("CO")) { diff --git a/src/core/worker.js b/src/core/worker.js index 7dee0f195d67fb..4bca2212707c66 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -524,6 +524,10 @@ class WorkerMessageHandler { return pdfManager.ensureDoc("fieldObjects"); }); + handler.on("HasJSActions", function (data) { + return pdfManager.ensureDoc("hasJSActions"); + }); + handler.on("GetCalculationOrderIds", function (data) { return pdfManager.ensureDoc("calculationOrderIds"); }); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index e293c7ca7f55e0..706a69fd25b365 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -142,6 +142,7 @@ class AnnotationElement { this.renderInteractiveForms = parameters.renderInteractiveForms; this.svgFactory = parameters.svgFactory; this.annotationStorage = parameters.annotationStorage; + this.enableScripting = parameters.enableScripting; if (isRenderable) { this.container = this._createContainer(ignoreBorder); @@ -478,7 +479,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { event.target.setSelectionRange(0, 0); }); - if (this.data.actions) { + if (this.enableScripting && this.data.documentHasJSActions) { element.addEventListener("updateFromSandbox", function (event) { const data = event.detail; if ("value" in data) { @@ -488,21 +489,23 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { } }); - 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; + if (this.data.actions !== null) { + 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; + } } } } @@ -1565,6 +1568,7 @@ class AnnotationLayer { svgFactory: new DOMSVGFactory(), annotationStorage: parameters.annotationStorage || new AnnotationStorage(), + enableScripting: parameters.enableScripting, }); if (element.isRenderable) { parameters.div.appendChild(element.render()); diff --git a/src/display/api.js b/src/display/api.js index a44ae82d1a1525..f5854770a1b874 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -903,6 +903,14 @@ class PDFDocumentProxy { return this._transport.getFieldObjects(); } + /** + * @returns {Promise} A promise that is resolved with true if + * there are some actions in JavaScript. + */ + hasJSActions() { + return this._transport.hasJSActions(); + } + /** * @returns {Promise | null>} A promise that is resolved with an * {Array} containing IDs of annotations that have a calculation @@ -2581,6 +2589,10 @@ class WorkerTransport { return this.messageHandler.sendWithPromise("GetFieldObjects", null); } + hasJSActions() { + return this.messageHandler.sendWithPromise("HasJSActions", null); + } + getCalculationOrderIds() { return this.messageHandler.sendWithPromise("GetCalculationOrderIds", null); } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 61879c7f00aef8..b6d4d93c867ea9 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -73,12 +73,14 @@ class Field extends PDFObject { // 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) - ); + if (data.actions !== null) { + 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) + ); + } } } diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index d212c467e338ea..ba06e487ddb1da 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -61,6 +61,10 @@ describe("annotation", function () { ensureCatalog(prop, args) { return this.ensure(this.pdfDocument.catalog, prop, args); } + + ensureDoc(prop, args) { + return this.ensure(this.pdfDocument, prop, args); + } } function HandlerMock() { diff --git a/test/unit/document_spec.js b/test/unit/document_spec.js index 5d3442a3d8fa68..57ab015421b6f4 100644 --- a/test/unit/document_spec.js +++ b/test/unit/document_spec.js @@ -239,5 +239,60 @@ describe("document", function () { expect(fields["parent.kid2"]).toEqual(["265R"]); expect(fields.parent).toEqual(["358R"]); }); + + it("should check if fields have any actions", async function () { + const acroForm = new Dict(); + + let pdfDocument = getDocument(acroForm); + let hasJSActions = await pdfDocument.hasJSActions; + expect(hasJSActions).toEqual(false); + + acroForm.set("Fields", []); + pdfDocument = getDocument(acroForm); + hasJSActions = await pdfDocument.hasJSActions; + expect(hasJSActions).toEqual(false); + + const kid1Ref = Ref.get(314, 0); + const kid11Ref = Ref.get(159, 0); + const kid2Ref = Ref.get(265, 0); + const parentRef = Ref.get(358, 0); + + const allFields = Object.create(null); + for (const name of ["parent", "kid1", "kid2", "kid11"]) { + const buttonWidgetDict = new Dict(); + buttonWidgetDict.set("Type", Name.get("Annot")); + buttonWidgetDict.set("Subtype", Name.get("Widget")); + buttonWidgetDict.set("FT", Name.get("Btn")); + buttonWidgetDict.set("T", name); + allFields[name] = buttonWidgetDict; + } + + allFields.kid1.set("Kids", [kid11Ref]); + allFields.parent.set("Kids", [kid1Ref, kid2Ref]); + + const xref = new XRefMock([ + { ref: parentRef, data: allFields.parent }, + { ref: kid1Ref, data: allFields.kid1 }, + { ref: kid11Ref, data: allFields.kid11 }, + { ref: kid2Ref, data: allFields.kid2 }, + ]); + + acroForm.set("Fields", [parentRef]); + pdfDocument = getDocument(acroForm, xref); + hasJSActions = await pdfDocument.hasJSActions; + expect(hasJSActions).toEqual(false); + + const JS = Name.get("JavaScript"); + const additionalActionsDict = new Dict(); + const eDict = new Dict(); + eDict.set("JS", "hello()"); + eDict.set("S", JS); + additionalActionsDict.set("E", eDict); + allFields.kid2.set("AA", additionalActionsDict); + + pdfDocument = getDocument(acroForm, xref); + hasJSActions = await pdfDocument.hasJSActions; + expect(hasJSActions).toEqual(true); + }); }); }); diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index ed50d0711e6b36..44f47ef8c1cbe9 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -28,6 +28,7 @@ import { SimpleLinkService } from "./pdf_link_service.js"; * @property {IPDFLinkService} linkService * @property {DownloadManager} downloadManager * @property {IL10n} l10n - Localization service. + * @property {boolean} enableScripting */ class AnnotationLayerBuilder { @@ -43,6 +44,7 @@ class AnnotationLayerBuilder { imageResourcesPath = "", renderInteractiveForms = true, l10n = NullL10n, + enableScripting = false, }) { this.pageDiv = pageDiv; this.pdfPage = pdfPage; @@ -52,6 +54,7 @@ class AnnotationLayerBuilder { this.renderInteractiveForms = renderInteractiveForms; this.l10n = l10n; this.annotationStorage = annotationStorage; + this.enableScripting = enableScripting; this.div = null; this._cancelled = false; @@ -82,6 +85,7 @@ class AnnotationLayerBuilder { linkService: this.linkService, downloadManager: this.downloadManager, annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, }; if (this.div) { @@ -126,6 +130,7 @@ class DefaultAnnotationLayerFactory { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -134,7 +139,8 @@ class DefaultAnnotationLayerFactory { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, - l10n = NullL10n + l10n = NullL10n, + enableScripting = false ) { return new AnnotationLayerBuilder({ pageDiv, @@ -144,6 +150,7 @@ class DefaultAnnotationLayerFactory { linkService: new SimpleLinkService(), l10n, annotationStorage, + enableScripting, }); } } diff --git a/web/app.js b/web/app.js index a414c4b7c90f85..d81f248684f523 100644 --- a/web/app.js +++ b/web/app.js @@ -449,6 +449,7 @@ const PDFViewerApplication = { enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), useOnlyCssZoom: AppOptions.get("useOnlyCssZoom"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"), + enableScripting: AppOptions.get("enableScripting"), }); pdfRenderingQueue.setViewer(this.pdfViewer); pdfLinkService.setViewer(this.pdfViewer); diff --git a/web/base_viewer.js b/web/base_viewer.js index 1a825078756ad2..ee8ba1a1e994fe 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -77,6 +77,7 @@ const DEFAULT_CACHE_SIZE = 10; * total pixels, i.e. width * height. Use -1 for no limit. The default value * is 4096 * 4096 (16 mega-pixels). * @property {IL10n} l10n - Localization service. + * @property {boolean} [enableScripting] - Enable JS evaluation. */ function PDFPageViewBuffer(size) { @@ -187,6 +188,7 @@ class BaseViewer { this.useOnlyCssZoom = options.useOnlyCssZoom || false; this.maxCanvasPixels = options.maxCanvasPixels; this.l10n = options.l10n || NullL10n; + this.enableScripting = options.enableScripting || false; this.defaultRenderingQueue = !options.renderingQueue; if (this.defaultRenderingQueue) { @@ -527,6 +529,7 @@ class BaseViewer { useOnlyCssZoom: this.useOnlyCssZoom, maxCanvasPixels: this.maxCanvasPixels, l10n: this.l10n, + enableScripting: this.enableScripting, }); this._pages.push(pageView); } @@ -1192,6 +1195,7 @@ class BaseViewer { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -1200,7 +1204,8 @@ class BaseViewer { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = false, - l10n = NullL10n + l10n = NullL10n, + enableScripting = false ) { return new AnnotationLayerBuilder({ pageDiv, @@ -1211,6 +1216,7 @@ class BaseViewer { linkService: this.linkService, downloadManager: this.downloadManager, l10n, + enableScripting, }); } diff --git a/web/interfaces.js b/web/interfaces.js index f13b11e5f9eb15..b1f35729bb1fcf 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -181,6 +181,7 @@ class IPDFAnnotationLayerFactory { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -189,7 +190,8 @@ class IPDFAnnotationLayerFactory { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, - l10n = undefined + l10n = undefined, + enableScripting = false ) {} } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 9b6eb3f021437d..fa322d741b9b66 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -63,6 +63,7 @@ import { viewerCompatibilityParams } from "./viewer_compatibility.js"; * total pixels, i.e. width * height. Use -1 for no limit. The default value * is 4096 * 4096 (16 mega-pixels). * @property {IL10n} l10n - Localization service. + * @property {boolean} enableScripting - Enable embedded script execution. */ const MAX_CANVAS_PIXELS = viewerCompatibilityParams.maxCanvasPixels || 16777216; @@ -109,6 +110,7 @@ class PDFPageView { this.renderer = options.renderer || RendererType.CANVAS; this.enableWebGL = options.enableWebGL || false; this.l10n = options.l10n || NullL10n; + this.enableScripting = options.enableScripting || false; this.paintTask = null; this.paintedViewportMap = new WeakMap(); @@ -549,7 +551,8 @@ class PDFPageView { this._annotationStorage, this.imageResourcesPath, this.renderInteractiveForms, - this.l10n + this.l10n, + this.enableScripting ); } this._renderAnnotationLayer(); diff --git a/web/ui_utils.js b/web/ui_utils.js index a35bcdfc6627db..6aeb386846f053 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -1018,7 +1018,10 @@ function getActiveOrFocusedElement() { */ function generateRandomStringForSandbox(objects) { const allObjects = Object.values(objects).flat(2); - const actions = allObjects.map(obj => Object.values(obj.actions)).flat(2); + const actions = allObjects + .filter(obj => !!obj.actions) + .map(obj => Object.values(obj.actions)) + .flat(2); while (true) { const name = new Uint8Array(64);