From 4aeff668907bf4f36c8008ceeb43bc02dc7116f1 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/document.js | 15 +++++++++ src/core/worker.js | 4 +++ src/display/annotation_layer.js | 41 ++++++++++++++---------- 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 | 15 ++++++++- web/app.js | 1 + web/base_viewer.js | 14 ++++++++- web/interfaces.js | 6 +++- web/pdf_page_view.js | 44 ++++++++++++++++---------- web/ui_utils.js | 5 ++- 13 files changed, 187 insertions(+), 43 deletions(-) diff --git a/src/core/document.js b/src/core/document.js index 19bd28977bc660..1c509b2351a559 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -1020,6 +1020,21 @@ class PDFDocument { ); } + get hasJSActions() { + return shadow( + this, + "hasJSActions", + this.fieldObjects.then(fieldObjects => { + return ( + fieldObjects !== null && + Object.values(fieldObjects).some(fieldObject => + fieldObject.some(object => object.actions !== null) + ) + ); + }) + ); + } + 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 bd19bf04a6c9ea..705568575e1908 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -142,6 +142,8 @@ class AnnotationElement { this.renderInteractiveForms = parameters.renderInteractiveForms; this.svgFactory = parameters.svgFactory; this.annotationStorage = parameters.annotationStorage; + this.enableScripting = parameters.enableScripting; + this.hasJSActions = parameters.hasJSActions; if (isRenderable) { this.container = this._createContainer(ignoreBorder); @@ -507,7 +509,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { event.target.setSelectionRange(0, 0); }); - if (this.data.actions) { + if (this.enableScripting && this.hasJSActions) { element.addEventListener("updateFromSandbox", function (event) { const data = event.detail; if ("value" in data) { @@ -517,21 +519,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; + } } } } @@ -1562,6 +1566,9 @@ class FileAttachmentAnnotationElement extends AnnotationElement { * @property {string} [imageResourcesPath] - Path for image resources, mainly * for annotation icons. Include trailing slash. * @property {boolean} renderInteractiveForms + * @property {boolean} [enableScripting] - Enable embedded script execution. + * @property {boolean} [hasJSActions] - Some fields have JS actions. + * The default value is `false`. */ class AnnotationLayer { @@ -1608,6 +1615,8 @@ class AnnotationLayer { svgFactory: new DOMSVGFactory(), annotationStorage: parameters.annotationStorage || new AnnotationStorage(), + enableScripting: parameters.enableScripting, + hasJSActions: parameters.hasJSActions, }); if (element.isRenderable) { const rendered = element.render(); diff --git a/src/display/api.js b/src/display/api.js index a44ae82d1a1525..0afce228d2f037 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 some fields have some JS actions. + */ + 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 79b7708ce34d0a..55a5157d240c58 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..e37ee831f012c4 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,8 @@ class AnnotationLayerBuilder { imageResourcesPath = "", renderInteractiveForms = true, l10n = NullL10n, + enableScripting = false, + hasJSActions = false, }) { this.pageDiv = pageDiv; this.pdfPage = pdfPage; @@ -52,6 +55,8 @@ class AnnotationLayerBuilder { this.renderInteractiveForms = renderInteractiveForms; this.l10n = l10n; this.annotationStorage = annotationStorage; + this.enableScripting = enableScripting; + this.hasJSActions = hasJSActions; this.div = null; this._cancelled = false; @@ -82,6 +87,8 @@ class AnnotationLayerBuilder { linkService: this.linkService, downloadManager: this.downloadManager, annotationStorage: this.annotationStorage, + enableScripting: this.enableScripting, + hasJSActions: this.hasJSActions, }; if (this.div) { @@ -126,6 +133,8 @@ class DefaultAnnotationLayerFactory { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting + * @param {boolean} hasJSActions * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -134,7 +143,9 @@ class DefaultAnnotationLayerFactory { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, - l10n = NullL10n + l10n = NullL10n, + enableScripting = false, + hasJSActions = false ) { return new AnnotationLayerBuilder({ pageDiv, @@ -144,6 +155,8 @@ class DefaultAnnotationLayerFactory { linkService: new SimpleLinkService(), l10n, annotationStorage, + enableScripting, + hasJSActions, }); } } diff --git a/web/app.js b/web/app.js index 731b9eab9a2acf..c76584643bd9d0 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 59da22496e7052..cf2c8552a98c25 100644 --- a/web/base_viewer.js +++ b/web/base_viewer.js @@ -77,6 +77,8 @@ 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 embedded script execution. + * The default value is `false`. */ function PDFPageViewBuffer(size) { @@ -187,6 +189,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) { @@ -465,6 +468,7 @@ class BaseViewer { const annotationStorage = pdfDocument.annotationStorage; const optionalContentConfigPromise = pdfDocument.getOptionalContentConfig(); + const hasJSActionsPromise = pdfDocument.hasJSActions(); this._pagesCapability.promise.then(() => { this.eventBus.dispatch("pagesloaded", { @@ -527,6 +531,8 @@ class BaseViewer { useOnlyCssZoom: this.useOnlyCssZoom, maxCanvasPixels: this.maxCanvasPixels, l10n: this.l10n, + enableScripting: this.enableScripting, + hasJSActionsPromise, }); this._pages.push(pageView); } @@ -1208,6 +1214,8 @@ class BaseViewer { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting + * @param {boolean} hasJSActions * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -1216,7 +1224,9 @@ class BaseViewer { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = false, - l10n = NullL10n + l10n = NullL10n, + enableScripting = false, + hasJSActions = false ) { return new AnnotationLayerBuilder({ pageDiv, @@ -1227,6 +1237,8 @@ class BaseViewer { linkService: this.linkService, downloadManager: this.downloadManager, l10n, + enableScripting, + hasJSActions, }); } diff --git a/web/interfaces.js b/web/interfaces.js index 62d7b13ebd6a6f..0623693621d4d4 100644 --- a/web/interfaces.js +++ b/web/interfaces.js @@ -181,6 +181,8 @@ class IPDFAnnotationLayerFactory { * for annotation icons. Include trailing slash. * @param {boolean} renderInteractiveForms * @param {IL10n} l10n + * @param {boolean} enableScripting + * @param {boolean} hasJSActions * @returns {AnnotationLayerBuilder} */ createAnnotationLayerBuilder( @@ -189,7 +191,9 @@ class IPDFAnnotationLayerFactory { annotationStorage = null, imageResourcesPath = "", renderInteractiveForms = true, - l10n = undefined + l10n = undefined, + enableScripting = false, + hasJSActions = false ) {} } diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 9b6eb3f021437d..2d28d9d8c29a7d 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -63,6 +63,10 @@ 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. + * The default value is `false`. + * @property {Promise} hasJSActionsPromise - A promise that is resolved + * with true if some fields have some JS actions. */ const MAX_CANVAS_PIXELS = viewerCompatibilityParams.maxCanvasPixels || 16777216; @@ -109,6 +113,8 @@ class PDFPageView { this.renderer = options.renderer || RendererType.CANVAS; this.enableWebGL = options.enableWebGL || false; this.l10n = options.l10n || NullL10n; + this.enableScripting = options.enableScripting || false; + this.hasJSActionsPromise = options.hasJSActionsPromise; this.paintTask = null; this.paintedViewportMap = new WeakMap(); @@ -541,26 +547,30 @@ class PDFPageView { } ); - if (this.annotationLayerFactory) { - if (!this.annotationLayer) { - this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder( - div, - pdfPage, - this._annotationStorage, - this.imageResourcesPath, - this.renderInteractiveForms, - this.l10n - ); + return this.hasJSActionsPromise.then(hasJSActions => { + if (this.annotationLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationLayerFactory.createAnnotationLayerBuilder( + div, + pdfPage, + this._annotationStorage, + this.imageResourcesPath, + this.renderInteractiveForms, + this.l10n, + this.enableScripting, + hasJSActions + ); + } + this._renderAnnotationLayer(); } - this._renderAnnotationLayer(); - } - div.setAttribute("data-loaded", true); + div.setAttribute("data-loaded", true); - this.eventBus.dispatch("pagerender", { - source: this, - pageNumber: this.id, + this.eventBus.dispatch("pagerender", { + source: this, + pageNumber: this.id, + }); + return resultPromise; }); - return resultPromise; } paintOnCanvas(canvasWrapper) { diff --git a/web/ui_utils.js b/web/ui_utils.js index 32481fd0b5cc0b..80577d03815a5b 100644 --- a/web/ui_utils.js +++ b/web/ui_utils.js @@ -1023,7 +1023,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);