From 94690de660d15be8245901a56d8288617863d037 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 30 Sep 2020 20:58:45 +0200 Subject: [PATCH] Add the possibility to collect Javascript actions --- src/core/annotation.js | 145 ++++++++++++++++++++++++++++++++++- src/core/document.js | 83 +++++++++++++++++++- src/core/worker.js | 4 + src/display/api.js | 12 +++ src/shared/compatibility.js | 9 +++ src/shared/util.js | 23 ++++++ test/unit/annotation_spec.js | 85 ++++++++++++++++++++ test/unit/document_spec.js | 9 +++ 8 files changed, 368 insertions(+), 2 deletions(-) diff --git a/src/core/annotation.js b/src/core/annotation.js index b0736ad6971895..112f5335773b21 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -14,12 +14,14 @@ */ import { + AnnotationActionEventType, AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, AnnotationReplyType, AnnotationType, assert, + bytesToString, escapeString, getModificationDate, isString, @@ -30,7 +32,15 @@ import { warn, } from "../shared/util.js"; import { Catalog, FileSpec, ObjectLoader } from "./obj.js"; -import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js"; +import { + Dict, + isDict, + isName, + isRef, + isStream, + Name, + RefSet, +} from "./primitives.js"; import { ColorSpace } from "./colorspace.js"; import { getInheritableProperty } from "./core_utils.js"; import { OperatorList } from "./operator_list.js"; @@ -569,6 +579,10 @@ class Annotation { return null; } + getAnnotationObject() { + return null; + } + /** * Reset the annotation. * @@ -903,6 +917,7 @@ class WidgetAnnotation extends Annotation { data.annotationType = AnnotationType.WIDGET; data.fieldName = this._constructFieldName(dict); + data.actions = this._collectActions(params.xref, dict); const fieldValue = getInheritableProperty({ dict, @@ -937,6 +952,7 @@ class WidgetAnnotation extends Annotation { } data.readOnly = this.hasFieldFlag(AnnotationFieldFlag.READONLY); + data.hidden = this.hasFieldFlag(AnnotationFieldFlag.HIDDEN); // Hide signatures because we cannot validate them, and unset the fieldValue // since it's (most likely) a `Dict` which is non-serializable and will thus @@ -944,6 +960,7 @@ class WidgetAnnotation extends Annotation { if (data.fieldType === "Sig") { data.fieldValue = null; this.setFlags(AnnotationFlag.HIDDEN); + data.hidden = true; } } @@ -1366,6 +1383,76 @@ class WidgetAnnotation extends Annotation { } return localResources || Dict.empty; } + + _collectJS(entry, xref, list, parents) { + if (!entry) { + return; + } + + let parent = null; + if (isRef(entry)) { + if (parents.has(entry)) { + // If we've already found entry then we've a cycle. + return; + } + parent = entry; + parents.put(parent); + entry = xref.fetch(entry); + } + if (Array.isArray(entry)) { + for (const element of entry) { + this._collectJS(element, xref, list, parents); + } + } else if (entry instanceof Dict) { + if (isName(entry.get("S"), "JavaScript") && entry.has("JS")) { + const js = entry.get("JS"); + let code; + if (isStream(js)) { + code = bytesToString(js.getBytes()); + } else { + code = stringToPDFString(js); + } + if (code) { + list.push(code); + } + } + this._collectJS(entry.getRaw("Next"), xref, list, parents); + } + + if (parent) { + parents.remove(parent); + } + } + + _collectActions(xref, dict) { + const actions = Object.create(null); + if (dict.has("AA")) { + const additionalAction = dict.get("AA"); + for (const key of additionalAction.getKeys()) { + if (key in AnnotationActionEventType) { + const actionDict = additionalAction.getRaw(key); + const parents = new RefSet(); + const list = []; + this._collectJS(actionDict, xref, list, parents); + if (list.length > 0) { + actions[AnnotationActionEventType[key]] = list; + } + } + } + } + return actions; + } + + getAnnotationObject() { + if (this.data.fieldType === "Sig") { + return { + id: this.data.id, + value: null, + type: "signature", + }; + } + return null; + } } class TextWidgetAnnotation extends WidgetAnnotation { @@ -1516,6 +1603,23 @@ class TextWidgetAnnotation extends WidgetAnnotation { return chunks; } + + getAnnotationObject() { + return { + id: this.data.id, + value: this.data.fieldValue, + multiline: this.data.multiline, + password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD), + charLimit: this.data.maxLen, + comb: this.data.comb, + editable: !this.data.readOnly, + hidden: this.data.hidden, + name: this.data.fieldName, + rect: this.data.rect, + actions: this.data.actions, + type: "text", + }; + } } class ButtonWidgetAnnotation extends WidgetAnnotation { @@ -1793,6 +1897,28 @@ class ButtonWidgetAnnotation extends WidgetAnnotation { docBaseUrl: params.pdfManager.docBaseUrl, }); } + + getAnnotationObject() { + let type = "button"; + let value = null; + if (this.data.checkBox) { + type = "checkbox"; + value = this.data.fieldValue && this.data.fieldValue !== "Off"; + } else if (this.data.radioButton) { + type = "radiobutton"; + value = this.data.fieldValue === this.data.buttonValue; + } + return { + id: this.data.id, + value, + editable: !this.data.readOnly, + name: this.data.fieldName, + rect: this.data.rect, + hidden: this.data.hidden, + actions: this.data.actions, + type, + }; + } } class ChoiceWidgetAnnotation extends WidgetAnnotation { @@ -1843,6 +1969,23 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation { this.data.multiSelect = this.hasFieldFlag(AnnotationFieldFlag.MULTISELECT); this._hasText = true; } + + getAnnotationObject() { + const type = this.data.combo ? "combobox" : "listbox"; + const value = + this.data.fieldValue.length > 0 ? this.data.fieldValue[0] : null; + return { + id: this.data.id, + value, + editable: !this.data.readOnly, + name: this.data.fieldName, + rect: this.data.rect, + multipleSelection: this.data.multiSelect, + hidden: this.data.hidden, + actions: this.data.actions, + type, + }; + } } class TextAnnotation extends MarkupAnnotation { diff --git a/src/core/document.js b/src/core/document.js index 51bc738c6fab05..6d9a2dce131b5d 100644 --- a/src/core/document.js +++ b/src/core/document.js @@ -226,6 +226,25 @@ class Page { return stream; } + getAnnotationObjects() { + // Fetch the page's annotations and get annotation data + // to be used in JS sandbox. + return this._parsedAnnotations.then(function (annotations) { + const results = []; + for (const annotation of annotations) { + if (!isAnnotationRenderable(annotation, "print")) { + continue; + } + const object = annotation.getAnnotationObject(); + if (object) { + results.push(object); + } + } + + return results; + }); + } + save(handler, task, annotationStorage) { const partialEvaluator = new PartialEvaluator({ xref: this.xref, @@ -708,7 +727,7 @@ class PDFDocument { } get formInfo() { - const formInfo = { hasAcroForm: false, hasXfa: false }; + const formInfo = { hasAcroForm: false, hasXfa: false, fields: null }; const acroForm = this.catalog.acroForm; if (!acroForm) { return shadow(this, "formInfo", formInfo); @@ -736,6 +755,9 @@ class PDFDocument { const hasOnlyDocumentSignatures = !!(sigFlags & 0x1) && this._hasOnlyDocumentSignatures(fields); formInfo.hasAcroForm = hasFields && !hasOnlyDocumentSignatures; + if (formInfo.hasAcroForm) { + formInfo.fields = fields; + } } catch (ex) { if (ex instanceof MissingDataException) { throw ex; @@ -935,6 +957,65 @@ class PDFDocument { ? this.catalog.cleanup(manuallyTriggered) : clearPrimitiveCaches(); } + + _collectFieldObjects(name, fieldRef, promises) { + const field = this.xref.fetchIfRef(fieldRef); + if (field.has("T")) { + const partName = stringToPDFString(field.get("T")); + if (name === "") { + name = partName; + } else { + name = `${name}.${partName}`; + } + } + + if (!(name in promises)) { + promises[name] = []; + } + promises[name].push( + AnnotationFactory.create( + this.xref, + fieldRef, + this.pdfManager, + this._localIdFactory + ) + .then(annotation => annotation.getAnnotationObject()) + .catch(function (reason) { + warn(`_collectFieldObjects: "${reason}".`); + return null; + }) + ); + + if (field.has("Kids")) { + const kids = field.get("Kids"); + for (const kid of kids) { + this._collectFieldObjects(name, kid, promises); + } + } + } + + async getFieldObjects() { + const formInfo = this.formInfo; + const allFields = Object.create(null); + if (!formInfo.hasAcroForm) { + return allFields; + } + + const fieldPromises = Object.create(null); + for (const fieldRef of formInfo.fields) { + this._collectFieldObjects("", fieldRef, fieldPromises); + } + + for (const [qualName, promises] of Object.entries(fieldPromises)) { + const fields = (await Promise.all(promises)).filter( + field => field !== null + ); + if (fields.length > 0) { + allFields[qualName] = fields; + } + } + return allFields; + } } export { Page, PDFDocument }; diff --git a/src/core/worker.js b/src/core/worker.js index 673b3f45cf291e..af5bffd8ea22f1 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -516,6 +516,10 @@ class WorkerMessageHandler { }); }); + handler.on("GetFieldObjects", function (data) { + return pdfManager.pdfDocument.getFieldObjects(); + }); + handler.on("SaveDocument", function ({ numPages, annotationStorage, diff --git a/src/display/api.js b/src/display/api.js index 0c184d8b2ad75b..be05381a432f8d 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -876,6 +876,14 @@ class PDFDocumentProxy { saveDocument(annotationStorage) { return this._transport.saveDocument(annotationStorage); } + + /** + * @returns {Promise>} A promise that is resolved with an + * {Array} containing annotation data. + */ + getFieldObjects() { + return this._transport.getFieldObjects(); + } } /** @@ -2549,6 +2557,10 @@ class WorkerTransport { }); } + getFieldObjects() { + return this.messageHandler.sendWithPromise("GetFieldObjects", null); + } + getDestinations() { return this.messageHandler.sendWithPromise("GetDestinations", null); } diff --git a/src/shared/compatibility.js b/src/shared/compatibility.js index 8a258fcd32f8a6..44f17a111610e4 100644 --- a/src/shared/compatibility.js +++ b/src/shared/compatibility.js @@ -86,6 +86,15 @@ if ( require("core-js/es/array/includes.js"); })(); + // Provides support for Array.prototype.flat in legacy browsers. + // Support: IE, Chrome<69 + (function checkArrayFlat() { + if (Array.prototype.flat) { + return; + } + require("core-js/es/array/flat.js"); + })(); + // Provides support for Array.from in legacy browsers. // Support: IE (function checkArrayFrom() { diff --git a/src/shared/util.js b/src/shared/util.js index 9b2b0edf11922e..d46e21a64df52f 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -309,6 +309,28 @@ const PasswordResponses = { INCORRECT_PASSWORD: 2, }; +const AnnotationActionEventType = { + E: "MouseEnter", + X: "MouseExit", + D: "MouseDown", + U: "MouseUp", + Fo: "Focus", + Bl: "Blur", + PO: "PageOpen", + PC: "PageClosed", + PV: "PageVisible", + PI: "PageInvisible", + K: "Keystroke", + F: "Format", + V: "Validate", + C: "Calculate", + WC: "WillClose", + WS: "WillSave", + DS: "DidSave", + WP: "WillPrint", + DP: "DidPrint", +}; + let verbosity = VerbosityLevel.WARNINGS; function setVerbosityLevel(level) { @@ -971,6 +993,7 @@ export { OPS, VerbosityLevel, UNSUPPORTED_FEATURES, + AnnotationActionEventType, AnnotationBorderStyleType, AnnotationFieldFlag, AnnotationFlag, diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 2d9093e8374557..ab6db2b46fcf99 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1863,6 +1863,91 @@ describe("annotation", function () { done(); }, done.fail); }); + + it("should get annotation object for use in pdf script", function (done) { + const textWidgetRef = Ref.get(123, 0); + const xDictRef = Ref.get(141, 0); + const dDictRef = Ref.get(262, 0); + const next0Ref = Ref.get(314, 0); + const next1Ref = Ref.get(271, 0); + const next2Ref = Ref.get(577, 0); + const next00Ref = Ref.get(413, 0); + const xDict = new Dict(); + const dDict = new Dict(); + const next0Dict = new Dict(); + const next1Dict = new Dict(); + const next2Dict = new Dict(); + const next00Dict = new Dict(); + + const xref = new XRefMock([ + { ref: textWidgetRef, data: textWidgetDict }, + { ref: xDictRef, data: xDict }, + { ref: dDictRef, data: dDict }, + { ref: next0Ref, data: next0Dict }, + { ref: next00Ref, data: next00Dict }, + { ref: next1Ref, data: next1Dict }, + { ref: next2Ref, data: next2Dict }, + ]); + + const JS = Name.get("JavaScript"); + const additionalActionDict = new Dict(); + const eDict = new Dict(); + eDict.set("JS", "hello()"); + eDict.set("S", JS); + additionalActionDict.set("E", eDict); + + xDict.set("JS", "world()"); + xDict.set("S", JS); + xDict.set("Next", [next0Ref, next1Ref, next2Ref, xDictRef]); + + next0Dict.set("JS", "olleh()"); + next0Dict.set("S", JS); + next0Dict.set("Next", next00Ref); + + next00Dict.set("JS", "foo()"); + next00Dict.set("S", JS); + next00Dict.set("Next", next0Ref); + + next1Dict.set("JS", "dlrow()"); + next1Dict.set("S", JS); + next1Dict.set("Next", xDictRef); + + next2Dict.set("JS", "oof()"); + next2Dict.set("S", JS); + + dDict.set("JS", "bar()"); + dDict.set("S", JS); + dDict.set("Next", dDictRef); + additionalActionDict.set("D", dDictRef); + + additionalActionDict.set("X", xDictRef); + textWidgetDict.set("AA", additionalActionDict); + + partialEvaluator.xref = xref; + + AnnotationFactory.create( + xref, + textWidgetRef, + pdfManagerMock, + idFactoryMock + ) + .then(annotation => { + return annotation.getAnnotationObject(); + }) + .then(object => { + const actions = object.actions; + expect(actions.MouseEnter).toEqual(["hello()"]); + expect(actions.MouseExit).toEqual([ + "world()", + "olleh()", + "foo()", + "dlrow()", + "oof()", + ]); + expect(actions.MouseDown).toEqual(["bar()"]); + done(); + }, done.fail); + }); }); describe("ButtonWidgetAnnotation", function () { diff --git a/test/unit/document_spec.js b/test/unit/document_spec.js index 0586898d72bd81..ba9968de9ee15b 100644 --- a/test/unit/document_spec.js +++ b/test/unit/document_spec.js @@ -63,6 +63,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: false, + fields: null, }); }); @@ -75,6 +76,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: false, + fields: null, }); acroForm.set("XFA", ["foo", "bar"]); @@ -82,6 +84,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: true, + fields: null, }); acroForm.set("XFA", new StringStream("")); @@ -89,6 +92,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: false, + fields: null, }); acroForm.set("XFA", new StringStream("non-empty")); @@ -96,6 +100,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: true, + fields: null, }); }); @@ -108,6 +113,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: false, + fields: null, }); acroForm.set("Fields", ["foo", "bar"]); @@ -115,6 +121,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: true, hasXfa: false, + fields: ["foo", "bar"], }); // If the first bit of the `SigFlags` entry is set and the `Fields` array @@ -125,6 +132,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: true, hasXfa: false, + fields: ["foo", "bar"], }); const annotationDict = new Dict(); @@ -147,6 +155,7 @@ describe("document", function () { expect(pdfDocument.formInfo).toEqual({ hasAcroForm: false, hasXfa: false, + fields: null, }); }); });