From 3f29892d63fb0fe438c80b67e5173cd9d0d70150 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 20 Apr 2021 19:21:52 +0200 Subject: [PATCH] [JS] Fix several issues found in pdf in #13269 - app.alert and few other function can use an object as parameter ({cMsg: ...}); - support app.alert with a question and a yes/no answer; - update field siblings when one is changed in an action; - stop calculation if calculate is set to false in the middle of calculations; - get a boolean for checkboxes when they've been set through annotationStorage instead of a string. --- src/display/annotation_layer.js | 14 ++++++-- src/pdf.sandbox.external.js | 6 ++++ src/scripting_api/app.js | 45 +++++++++++++++++++++--- src/scripting_api/doc.js | 37 ++++++++++++++++---- src/scripting_api/event.js | 51 ++++++++++++++++++---------- src/scripting_api/field.js | 14 ++++++-- src/scripting_api/initialization.js | 30 +++++++++++----- src/scripting_api/proxy.js | 15 +++++++- test/integration/scripting_spec.js | 41 +++++++++++++++++++++- test/pdfs/.gitignore | 1 + test/pdfs/issue13269.pdf | Bin 0 -> 6499 bytes web/pdf_scripting_manager.js | 20 ++++++----- 12 files changed, 224 insertions(+), 50 deletions(-) create mode 100644 test/pdfs/issue13269.pdf diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 6e25f44b6..b8fdc21b7 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -922,12 +922,17 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { const storage = this.annotationStorage; const data = this.data; const id = data.id; - const value = storage.getValue(id, { + let value = storage.getValue(id, { value: data.fieldValue && ((data.exportValue && data.exportValue === data.fieldValue) || (!data.exportValue && data.fieldValue !== "Off")), }).value; + if (typeof value === "string") { + // The value has been changed through js and set in annotationStorage. + value = value !== "Off"; + storage.setValue(id, { value }); + } this.container.className = "buttonWidgetAnnotation checkBox"; @@ -1012,9 +1017,14 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { const storage = this.annotationStorage; const data = this.data; const id = data.id; - const value = storage.getValue(id, { + let value = storage.getValue(id, { value: data.fieldValue === data.buttonValue, }).value; + if (typeof value === "string") { + // The value has been changed through js and set in annotationStorage. + value = value !== data.buttonValue; + storage.setValue(id, { value }); + } const element = document.createElement("input"); element.disabled = data.readOnly; diff --git a/src/pdf.sandbox.external.js b/src/pdf.sandbox.external.js index 38b9faaeb..3fda62553 100644 --- a/src/pdf.sandbox.external.js +++ b/src/pdf.sandbox.external.js @@ -117,6 +117,12 @@ class SandboxSupportBase { } this.win.alert(cMsg); }, + confirm: cMsg => { + if (typeof cMsg !== "string") { + return false; + } + return this.win.confirm(cMsg); + }, prompt: (cQuestion, cDefault) => { if (typeof cQuestion !== "string" || typeof cDefault !== "string") { return null; diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index ddc0682ca..53418eb4f 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -28,8 +28,6 @@ class App extends PDFObject { constructor(data) { super(data); - this.calculate = true; - this._constants = null; this._focusRect = true; this._fs = null; @@ -68,6 +66,7 @@ class App extends PDFObject { this._timeoutCallbackId = 0; this._globalEval = data.globalEval; this._externalCall = data.externalCall; + this._document = data._document; } // This function is called thanks to the proxy @@ -191,6 +190,14 @@ class App extends PDFObject { throw new Error("app.activeDocs is read-only"); } + get calculate() { + return this._document.obj.calculate; + } + + set calculate(calculate) { + this._document.obj.calculate = calculate; + } + get constants() { if (!this._constants) { this._constants = Object.freeze({ @@ -427,7 +434,21 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { + if (typeof cMsg === "object") { + nType = cMsg.nType; + cMsg = cMsg.cMsg; + } + cMsg = (cMsg || "").toString(); + nType = + typeof nType !== "number" || isNaN(nType) || nType < 0 || nType > 3 + ? 0 + : nType; + if (nType >= 2) { + return this._externalCall("confirm", [cMsg]) ? 4 : 3; + } + this._externalCall("alert", [cMsg]); + return 1; } beep() { @@ -543,10 +564,21 @@ class App extends PDFObject { } response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { + if (typeof cQuestion === "object") { + cDefault = cQuestion.cDefault; + cQuestion = cQuestion.cQuestion; + } + cQuestion = (cQuestion || "").toString(); + cDefault = (cDefault || "").toString(); return this._externalCall("prompt", [cQuestion, cDefault || ""]); } - setInterval(cExpr, nMilliseconds) { + setInterval(cExpr, nMilliseconds = 0) { + if (typeof cExpr === "object") { + nMilliseconds = cExpr.nMilliseconds || 0; + cExpr = cExpr.cExpr; + } + if (typeof cExpr !== "string") { throw new TypeError("First argument of app.setInterval must be a string"); } @@ -560,7 +592,12 @@ class App extends PDFObject { return this._registerTimeout(callbackId, true); } - setTimeOut(cExpr, nMilliseconds) { + setTimeOut(cExpr, nMilliseconds = 0) { + if (typeof cExpr === "object") { + nMilliseconds = cExpr.nMilliseconds || 0; + cExpr = cExpr.cExpr; + } + if (typeof cExpr !== "string") { throw new TypeError("First argument of app.setTimeOut must be a string"); } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 96cbfe413..c3ca6e226 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -820,6 +820,9 @@ class Doc extends PDFObject { } getField(cName) { + if (typeof cName === "object") { + cName = cName.cName; + } if (typeof cName !== "string") { throw new TypeError("Invalid field name: must be a string"); } @@ -852,7 +855,7 @@ class Doc extends PDFObject { } } - return undefined; + return null; } _getChildren(fieldName) { @@ -885,6 +888,9 @@ class Doc extends PDFObject { } getNthFieldName(nIndex) { + if (typeof nIndex === "object") { + nIndex = nIndex.nIndex; + } if (typeof nIndex !== "number") { throw new TypeError("Invalid field index: must be a number"); } @@ -1020,6 +1026,18 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { + if (typeof bUI === "object") { + nStart = bUI.nStart; + nEnd = bUI.nEnd; + bSilent = bUI.bSilent; + bShrinkToFit = bUI.bShrinkToFit; + bPrintAsImage = bUI.bPrintAsImage; + bReverse = bUI.bReverse; + bAnnotations = bUI.bAnnotations; + printParams = bUI.printParams; + bUI = bUI.bUI; + } + // TODO: for now just use nStart and nEnd // so need to see how to deal with the other params // (if possible) @@ -1084,15 +1102,22 @@ class Doc extends PDFObject { } resetForm(aFields = null) { + if (aFields && !Array.isArray(aFields) && typeof aFields === "object") { + aFields = aFields.aFields; + } let mustCalculate = false; if (aFields) { for (const fieldName of aFields) { - const field = this.getField(fieldName); - if (field) { - field.value = field.defaultValue; - field.valueAsString = field.value; - mustCalculate = true; + if (!fieldName) { + continue; } + const field = this.getField(fieldName); + if (!field) { + continue; + } + field.value = field.defaultValue; + field.valueAsString = field.value; + mustCalculate = true; } } else { mustCalculate = this._fields.size !== 0; diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index c49c45302..baec46215 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -96,23 +96,33 @@ class EventDispatcher { } } - if (name === "Keystroke") { - savedChange = { - value: event.value, - change: event.change, - selStart: event.selStart, - selEnd: event.selEnd, - }; - } else if (name === "Blur" || name === "Focus") { - Object.defineProperty(event, "value", { - configurable: false, - writable: false, - enumerable: true, - value: event.value, - }); - } else if (name === "Validate") { - this.runValidation(source, event); - return; + switch (name) { + case "Keystroke": + savedChange = { + value: event.value, + change: event.change, + selStart: event.selStart, + selEnd: event.selEnd, + }; + break; + case "Blur": + case "Focus": + Object.defineProperty(event, "value", { + configurable: false, + writable: false, + enumerable: true, + value: event.value, + }); + break; + case "Validate": + this.runValidation(source, event); + return; + case "Action": + this.runActions(source, source, event, name); + if (this._document.obj.calculate) { + this.runCalculate(source, event); + } + return; } this.runActions(source, source, event, name); @@ -143,8 +153,10 @@ class EventDispatcher { if (event.rc) { if (hasRan) { source.wrapped.value = event.value; + source.wrapped.valueAsString = event.value; } else { source.obj.value = event.value; + source.obj.valueAsString = event.value; } if (this._document.obj.calculate) { @@ -187,6 +199,11 @@ class EventDispatcher { continue; } + if (!this._document.obj.calculate) { + // An action may have changed calculate value. + continue; + } + event.value = null; const target = this._objects[targetId]; this.runActions(source, target, event, "Calculate"); diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index e7154dc8c..c7de3124e 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -81,12 +81,14 @@ class Field extends PDFObject { this._strokeColor = data.strokeColor || ["G", 0]; this._textColor = data.textColor || ["G", 0]; this._value = data.value || ""; - this._valueAsString = data.valueAsString; this._kidIds = data.kidIds || null; this._fieldType = getFieldType(this._actions); + this._siblings = data.siblings || null; this._globalEval = data.globalEval; this._appObjects = data.appObjects; + + this.valueAsString = data.valueAsString || this._value; } get currentValueIndices() { @@ -246,6 +248,9 @@ class Field extends PDFObject { } get valueAsString() { + if (this._valueAsString === undefined) { + this._valueAsString = this._value ? this._value.toString() : ""; + } return this._valueAsString; } @@ -286,6 +291,9 @@ class Field extends PDFObject { } 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... + // We should probably have an html button for this annotation. } buttonSetIcon(oIcon, nFace = 0) { @@ -512,7 +520,7 @@ class RadioButtonField extends Field { } set value(value) { - if (value === null) { + if (value === null || value === undefined) { this._value = ""; } const i = this.exportValues.indexOf(value); @@ -574,7 +582,7 @@ class CheckboxField extends RadioButtonField { } set value(value) { - if (value === "Off") { + if (!value || value === "Off") { this._value = "Off"; } else { super.value = value; diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index f1c4484e7..ad33b86a3 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -94,15 +94,29 @@ function initSandbox(params) { obj.doc = _document; obj.fieldPath = name; obj.appObjects = appObjects; + let field; - if (obj.type === "radiobutton") { - const otherButtons = annotations.slice(1); - field = new RadioButtonField(otherButtons, obj); - } else if (obj.type === "checkbox") { - const otherButtons = annotations.slice(1); - field = new CheckboxField(otherButtons, obj); - } else { - field = new Field(obj); + switch (obj.type) { + case "radiobutton": { + const otherButtons = annotations.slice(1); + field = new RadioButtonField(otherButtons, obj); + break; + } + case "checkbox": { + const otherButtons = annotations.slice(1); + field = new CheckboxField(otherButtons, obj); + break; + } + case "text": + if (annotations.length <= 1) { + field = new Field(obj); + break; + } + obj.siblings = annotations.map(x => x.id).slice(1); + field = new Field(obj); + break; + default: + field = new Field(obj); } const wrapped = new Proxy(field, proxyHandler); diff --git a/src/scripting_api/proxy.js b/src/scripting_api/proxy.js index 44d0ec32f..41e6fb336 100644 --- a/src/scripting_api/proxy.js +++ b/src/scripting_api/proxy.js @@ -38,6 +38,14 @@ class ProxyHandler { } 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; @@ -46,7 +54,12 @@ class ProxyHandler { data[prop] = obj[prop]; // send the updated value to the other side - obj._send(data); + if (!obj._siblings) { + obj._send(data); + } else { + data.siblings = obj._siblings; + obj._send(data); + } } } else { obj._expandos[prop] = value; diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index be45fee7f..99c31f464 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -787,7 +787,7 @@ describe("Interaction", () => { ` ['Text1', 'Text2', 'Text4', 'List Box7', 'Group6'].map(x => this.getField(x).page).join(',') - ` + ` ); // Click on execute button to eval the above code. @@ -802,4 +802,43 @@ describe("Interaction", () => { ); }); }); + + describe("in issue13269.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue13269.pdf", "#\\32 7R"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must update fields with the same name from JS", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 7R", "hello"); + await page.keyboard.press("Enter"); + + await Promise.all( + [4, 5, 6].map(async n => + page.waitForFunction( + `document.querySelector("#\\\\32 ${n}R").value !== ""` + ) + ) + ); + + const expected = "hello world"; + for (const n of [4, 5, 6]) { + const text = await page.$eval(`#\\32 ${n}R`, el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual(expected); + } + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 43d39f2d3..12fbc208e 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -8,6 +8,7 @@ !franz_2.pdf !fraction-highlight.pdf !german-umlaut-r.pdf +!issue13269.pdf !xref_command_missing.pdf !issue1155r.pdf !issue2017r.pdf diff --git a/test/pdfs/issue13269.pdf b/test/pdfs/issue13269.pdf new file mode 100644 index 0000000000000000000000000000000000000000..c893ea374e76e5aaf3e3d5e2d1826215099431e8 GIT binary patch literal 6499 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 mC<5#5`_Jw@yy|huus{zlKM(HB9wXeK