From 094ff38da0e602daed46b4a2e2ab1c3659892de0 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 2 May 2022 19:28:00 +0200 Subject: [PATCH 1/2] =?UTF-8?q?[JS]=20Fix=20few=20bugs=20present=20in=20th?= =?UTF-8?q?e=20pdf=20for=20issue=20#14862=20-=20since=20resetForm=20functi?= =?UTF-8?q?on=20reset=20a=20field=20value=20a=20calculateNow=20is=20conseq?= =?UTF-8?q?uently=20triggered.=20=20=20But=20the=20calculate=20callback=20?= =?UTF-8?q?can=20itself=20call=20resetForm,=20hence=20an=20infinite=20recu?= =?UTF-8?q?rsive=20loop.=20=20=20So=20basically,=20prevent=20calculeNow=20?= =?UTF-8?q?to=20be=20triggered=20by=20itself.=20-=20in=20Firefox,=20the=20?= =?UTF-8?q?letters=20entered=20in=20some=20fields=20were=20duplicated:=20"?= =?UTF-8?q?AaBb"=20instead=20of=20"AB".=20=20=20It=20was=20mainly=20becaus?= =?UTF-8?q?e=20beforeInput=20was=20triggering=20a=20Keystroke=20which=20wa?= =?UTF-8?q?s=20itself=20triggering=20=20=20an=20input=20value=20update=20a?= =?UTF-8?q?nd=20then=20the=20input=20event=20was=20triggered.=20=20=20So?= =?UTF-8?q?=20in=20order=20to=20avoid=20that,=20beforeInput=20calls=20prev?= =?UTF-8?q?entDefault=20and=20then=20it's=20up=20to=20the=20JS=20to=20=20?= =?UTF-8?q?=20handle=20the=20event.=20-=20fields=20have=20a=20property=20v?= =?UTF-8?q?alueAsString=20which=20returns=20the=20value=20as=20a=20string.?= =?UTF-8?q?=20In=20the=20=20=20implementation=20it=20was=20wrongly=20used?= =?UTF-8?q?=20to=20store=20the=20formatted=20value=20of=20a=20field=20(2?= =?UTF-8?q?=E2=82=AC=20when=20the=20user=20=20=20entered=202).=20So=20this?= =?UTF-8?q?=20patch=20implements=20correctly=20valueAsString.=20-=20non-re?= =?UTF-8?q?ndered=20fields=20can=20be=20updated=20in=20using=20JS=20but=20?= =?UTF-8?q?when=20they're,=20they=20must=20take=20some=20properties=20=20?= =?UTF-8?q?=20in=20the=20annotationStorage.=20It=20was=20implemented=20for?= =?UTF-8?q?=20field=20values,=20but=20it=20wasn't=20for=20=20=20display,?= =?UTF-8?q?=20colors,=20...=20-=20it=20fixes=20#14862=20and=20#14705.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/annotation.js | 5 +- src/display/annotation_layer.js | 291 +++++++++++++++++----------- src/display/annotation_storage.js | 12 ++ src/scripting_api/app.js | 8 +- src/scripting_api/doc.js | 56 ++++-- src/scripting_api/event.js | 149 +++++++++----- src/scripting_api/field.js | 17 +- src/scripting_api/initialization.js | 2 +- test/integration/scripting_spec.js | 173 ++++++++++++++++- test/pdfs/.gitignore | 2 + test/pdfs/issue14705.pdf | Bin 0 -> 7755 bytes test/pdfs/issue14862.pdf | Bin 0 -> 13634 bytes test/unit/api_spec.js | 2 +- test/unit/scripting_spec.js | 42 ++-- 14 files changed, 556 insertions(+), 203 deletions(-) create mode 100644 test/pdfs/issue14705.pdf create mode 100755 test/pdfs/issue14862.pdf diff --git a/src/core/annotation.js b/src/core/annotation.js index 195a9cf1ee4a7..6f6c533e13f77 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -1518,7 +1518,8 @@ class WidgetAnnotation extends Annotation { const storageEntry = annotationStorage ? annotationStorage.get(this.data.id) : undefined; - let value = storageEntry && storageEntry.value; + let value = + storageEntry && (storageEntry.formattedValue || storageEntry.value); if (value === undefined) { if (!this._hasValueFromXFA || this.appearance) { // The annotation hasn't been rendered so use the appearance. @@ -1981,7 +1982,7 @@ class TextWidgetAnnotation extends WidgetAnnotation { return { id: this.data.id, value: this.data.fieldValue, - defaultValue: this.data.defaultFieldValue, + defaultValue: this.data.defaultFieldValue || "", multiline: this.data.multiLine, password: this.hasFieldFlag(AnnotationFieldFlag.PASSWORD), charLimit: this.data.maxLen, diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 31edf3580656b..8332a2f796369 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -297,6 +297,110 @@ class AnnotationElement { return container; } + get _commonActions() { + const setColor = (jsName, styleName, event) => { + const color = event.detail[jsName]; + event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`]( + color.slice(1) + ); + }; + + return shadow(this, "_commonActions", { + display: event => { + const hidden = event.detail.display % 2 === 1; + event.target.style.visibility = hidden ? "hidden" : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden, + print: event.detail.display === 0 || event.detail.display === 3, + }); + }, + print: event => { + this.annotationStorage.setValue(this.data.id, { + print: event.detail.print, + }); + }, + hidden: event => { + event.target.style.visibility = event.detail.hidden + ? "hidden" + : "visible"; + this.annotationStorage.setValue(this.data.id, { + hidden: event.detail.hidden, + }); + }, + focus: event => { + setTimeout(() => event.target.focus({ preventScroll: false }), 0); + }, + userName: event => { + // tooltip + event.target.title = event.detail.userName; + }, + readonly: event => { + if (event.detail.readonly) { + event.target.setAttribute("readonly", ""); + } else { + event.target.removeAttribute("readonly"); + } + }, + required: event => { + if (event.detail.required) { + event.target.setAttribute("required", ""); + } else { + event.target.removeAttribute("required"); + } + }, + bgColor: event => { + setColor("bgColor", "backgroundColor", event); + }, + fillColor: event => { + setColor("fillColor", "backgroundColor", event); + }, + fgColor: event => { + setColor("fgColor", "color", event); + }, + textColor: event => { + setColor("textColor", "color", event); + }, + borderColor: event => { + setColor("borderColor", "borderColor", event); + }, + strokeColor: event => { + setColor("strokeColor", "borderColor", event); + }, + }); + } + + _dispatchEventFromSandbox(actions, jsEvent) { + const commonActions = this._commonActions; + for (const name of Object.keys(jsEvent.detail)) { + const action = actions[name] || commonActions[name]; + if (action) { + action(jsEvent); + } + } + } + + _setDefaultPropertiesFromJS(element) { + if (!this.enableScripting) { + return; + } + + // Some properties may have been updated thanks to JS. + const storedData = this.annotationStorage.getRawValue(this.data.id); + if (!storedData) { + return; + } + + const commonActions = this._commonActions; + for (const [actionName, detail] of Object.entries(storedData)) { + const action = commonActions[actionName]; + if (action) { + action({ detail, target: element }); + // The action has been consumed: no need to keep it. + delete storedData[actionName]; + } + } + } + /** * Create quadrilaterals from the annotation's quadpoints. * @@ -657,7 +761,7 @@ class LinkAnnotationElement extends AnnotationElement { switch (field.type) { case "text": { const value = field.defaultValue || ""; - storage.setValue(id, { value, valueAsString: value }); + storage.setValue(id, { value }); break; } case "checkbox": @@ -794,85 +898,6 @@ class WidgetAnnotationElement extends AnnotationElement { ? "transparent" : Util.makeHexColor(color[0], color[1], color[2]); } - - _dispatchEventFromSandbox(actions, jsEvent) { - const setColor = (jsName, styleName, event) => { - const color = event.detail[jsName]; - event.target.style[styleName] = ColorConverters[`${color[0]}_HTML`]( - color.slice(1) - ); - }; - - const commonActions = { - display: event => { - const hidden = event.detail.display % 2 === 1; - event.target.style.visibility = hidden ? "hidden" : "visible"; - this.annotationStorage.setValue(this.data.id, { - hidden, - print: event.detail.display === 0 || event.detail.display === 3, - }); - }, - print: event => { - this.annotationStorage.setValue(this.data.id, { - print: event.detail.print, - }); - }, - hidden: event => { - event.target.style.visibility = event.detail.hidden - ? "hidden" - : "visible"; - this.annotationStorage.setValue(this.data.id, { - hidden: event.detail.hidden, - }); - }, - focus: event => { - setTimeout(() => event.target.focus({ preventScroll: false }), 0); - }, - userName: event => { - // tooltip - event.target.title = event.detail.userName; - }, - readonly: event => { - if (event.detail.readonly) { - event.target.setAttribute("readonly", ""); - } else { - event.target.removeAttribute("readonly"); - } - }, - required: event => { - if (event.detail.required) { - event.target.setAttribute("required", ""); - } else { - event.target.removeAttribute("required"); - } - }, - bgColor: event => { - setColor("bgColor", "backgroundColor", event); - }, - fillColor: event => { - setColor("fillColor", "backgroundColor", event); - }, - fgColor: event => { - setColor("fgColor", "color", event); - }, - textColor: event => { - setColor("textColor", "color", event); - }, - borderColor: event => { - setColor("borderColor", "borderColor", event); - }, - strokeColor: event => { - setColor("strokeColor", "borderColor", event); - }, - }; - - for (const name of Object.keys(jsEvent.detail)) { - const action = actions[name] || commonActions[name]; - if (action) { - action(jsEvent); - } - } - } } class TextWidgetAnnotationElement extends WidgetAnnotationElement { @@ -909,12 +934,12 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { // from parsing the elements correctly for the reference tests. const storedData = storage.getValue(id, { value: this.data.fieldValue, - valueAsString: this.data.fieldValue, }); - const textContent = storedData.valueAsString || storedData.value || ""; + const textContent = storedData.formattedValue || storedData.value || ""; const elementData = { userValue: null, formattedValue: null, + valueOnFocus: "", }; if (this.data.multiLine) { @@ -944,14 +969,15 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { }); element.addEventListener("resetform", event => { - const defaultValue = this.data.defaultFieldValue || ""; + const defaultValue = this.data.defaultFieldValue ?? ""; element.value = elementData.userValue = defaultValue; - delete elementData.formattedValue; + elementData.formattedValue = null; }); let blurListener = event => { - if (elementData.formattedValue) { - event.target.value = elementData.formattedValue; + const { formattedValue } = elementData; + if (formattedValue !== null && formattedValue !== undefined) { + event.target.value = formattedValue; } // Reset the cursor position to the start of the field (issue 12359). event.target.scrollLeft = 0; @@ -962,32 +988,33 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (elementData.userValue) { event.target.value = elementData.userValue; } + elementData.valueOnFocus = event.target.value; }); element.addEventListener("updatefromsandbox", jsEvent => { const actions = { value(event) { - elementData.userValue = event.detail.value || ""; + elementData.userValue = event.detail.value ?? ""; storage.setValue(id, { value: elementData.userValue.toString() }); - if (!elementData.formattedValue) { - event.target.value = elementData.userValue; - } + event.target.value = elementData.userValue; }, - valueAsString(event) { - elementData.formattedValue = event.detail.valueAsString || ""; - if (event.target !== document.activeElement) { + formattedValue(event) { + const { formattedValue } = event.detail; + elementData.formattedValue = formattedValue; + if ( + formattedValue !== null && + formattedValue !== undefined && + event.target !== document.activeElement + ) { // Input hasn't the focus so display formatted string - event.target.value = elementData.formattedValue; + event.target.value = formattedValue; } storage.setValue(id, { - formattedValue: elementData.formattedValue, + formattedValue, }); }, selRange(event) { - const [selStart, selEnd] = event.detail.selRange; - if (selStart >= 0 && selEnd < event.target.value.length) { - event.target.setSelectionRange(selStart, selEnd); - } + event.target.setSelectionRange(...event.detail.selRange); }, }; this._dispatchEventFromSandbox(actions, jsEvent); @@ -1009,14 +1036,18 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (commitKey === -1) { return; } + const { value } = event.target; + if (elementData.valueOnFocus === value) { + return; + } // Save the entered value - elementData.userValue = event.target.value; + elementData.userValue = value; this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", - value: event.target.value, + value, willCommit: true, commitKey, selStart: event.target.selectionStart, @@ -1027,15 +1058,16 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { const _blurListener = blurListener; blurListener = null; element.addEventListener("blur", event => { - elementData.userValue = event.target.value; - if (this._mouseState.isDown) { + const { value } = event.target; + elementData.userValue = value; + if (this._mouseState.isDown && elementData.valueOnFocus !== value) { // Focus out using the mouse: data are committed this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", - value: event.target.value, + value, willCommit: true, commitKey: 1, selStart: event.target.selectionStart, @@ -1048,19 +1080,56 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { if (this.data.actions?.Keystroke) { element.addEventListener("beforeinput", event => { - elementData.formattedValue = ""; const { data, target } = event; const { value, selectionStart, selectionEnd } = target; + + let selStart = selectionStart, + selEnd = selectionEnd; + + switch (event.inputType) { + // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes + case "deleteWordBackward": { + const match = value + .substring(0, selectionStart) + .match(/\w*[^\w]*$/); + if (match) { + selStart -= match[0].length; + } + break; + } + case "deleteWordForward": { + const match = value + .substring(selectionStart) + .match(/^[^\w]*\w*/); + if (match) { + selEnd += match[0].length; + } + break; + } + case "deleteContentBackward": + if (selectionStart === selectionEnd) { + selStart -= 1; + } + break; + case "deleteContentForward": + if (selectionStart === selectionEnd) { + selEnd += 1; + } + break; + } + + // We handle the event ourselves. + event.preventDefault(); this.linkService.eventBus?.dispatch("dispatcheventinsandbox", { source: this, detail: { id, name: "Keystroke", value, - change: data, + change: data || "", willCommit: false, - selStart: selectionStart, - selEnd: selectionEnd, + selStart, + selEnd, }, }); }); @@ -1104,6 +1173,7 @@ class TextWidgetAnnotationElement extends WidgetAnnotationElement { this._setTextStyle(element); this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1213,6 +1283,7 @@ class CheckboxWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1300,6 +1371,7 @@ class RadioButtonWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(element); + this._setDefaultPropertiesFromJS(element); this.container.appendChild(element); return this.container; @@ -1322,6 +1394,8 @@ class PushButtonWidgetAnnotationElement extends LinkAnnotationElement { container.title = this.data.alternativeText; } + this._setDefaultPropertiesFromJS(container); + return container; } } @@ -1534,6 +1608,7 @@ class ChoiceWidgetAnnotationElement extends WidgetAnnotationElement { } this._setBackgroundColor(selectElement); + this._setDefaultPropertiesFromJS(selectElement); this.container.appendChild(selectElement); return this.container; diff --git a/src/display/annotation_storage.js b/src/display/annotation_storage.js index def437187fdc4..43099079b62ab 100644 --- a/src/display/annotation_storage.js +++ b/src/display/annotation_storage.js @@ -50,6 +50,18 @@ class AnnotationStorage { return Object.assign(defaultValue, value); } + /** + * Get the value for a given key. + * + * @public + * @memberof AnnotationStorage + * @param {string} key + * @returns {Object} + */ + getRawValue(key) { + return this._storage.get(key); + } + /** * Set the value for a given key * diff --git a/src/scripting_api/app.js b/src/scripting_api/app.js index 46ec5ba36f6f6..9f94aa672a7d7 100644 --- a/src/scripting_api/app.js +++ b/src/scripting_api/app.js @@ -434,7 +434,7 @@ class App extends PDFObject { oDoc = null, oCheckbox = null ) { - if (typeof cMsg === "object") { + if (cMsg && typeof cMsg === "object") { nType = cMsg.nType; cMsg = cMsg.cMsg; } @@ -580,7 +580,7 @@ class App extends PDFObject { } response(cQuestion, cTitle = "", cDefault = "", bPassword = "", cLabel = "") { - if (typeof cQuestion === "object") { + if (cQuestion && typeof cQuestion === "object") { cDefault = cQuestion.cDefault; cQuestion = cQuestion.cQuestion; } @@ -590,7 +590,7 @@ class App extends PDFObject { } setInterval(cExpr, nMilliseconds = 0) { - if (typeof cExpr === "object") { + if (cExpr && typeof cExpr === "object") { nMilliseconds = cExpr.nMilliseconds || 0; cExpr = cExpr.cExpr; } @@ -609,7 +609,7 @@ class App extends PDFObject { } setTimeOut(cExpr, nMilliseconds = 0) { - if (typeof cExpr === "object") { + if (cExpr && typeof cExpr === "object") { nMilliseconds = cExpr.nMilliseconds || 0; cExpr = cExpr.cExpr; } diff --git a/src/scripting_api/doc.js b/src/scripting_api/doc.js index 905f9cfec33f3..153264393d821 100644 --- a/src/scripting_api/doc.js +++ b/src/scripting_api/doc.js @@ -820,8 +820,8 @@ class Doc extends PDFObject { /* Not implemented */ } - getField(cName) { - if (typeof cName === "object") { + _getField(cName) { + if (cName && typeof cName === "object") { cName = cName.cName; } if (typeof cName !== "string") { @@ -859,6 +859,14 @@ class Doc extends PDFObject { return null; } + getField(cName) { + const field = this._getField(cName); + if (!field) { + return null; + } + return field.wrapped; + } + _getChildren(fieldName) { // Children of foo.bar are foo.bar.oof, foo.bar.rab // but not foo.bar.oof.FOO. @@ -889,7 +897,7 @@ class Doc extends PDFObject { } getNthFieldName(nIndex) { - if (typeof nIndex === "object") { + if (nIndex && typeof nIndex === "object") { nIndex = nIndex.nIndex; } if (typeof nIndex !== "number") { @@ -1027,7 +1035,7 @@ class Doc extends PDFObject { bAnnotations = true, printParams = null ) { - if (typeof bUI === "object") { + if (bUI && typeof bUI === "object") { nStart = bUI.nStart; nEnd = bUI.nEnd; bSilent = bUI.bSilent; @@ -1103,30 +1111,52 @@ class Doc extends PDFObject { } resetForm(aFields = null) { - if (aFields && !Array.isArray(aFields) && typeof aFields === "object") { + // Handle the case resetForm({ aFields: ... }) + if (aFields && typeof aFields === "object") { aFields = aFields.aFields; } + + if (aFields && !Array.isArray(aFields)) { + aFields = [aFields]; + } + let mustCalculate = false; + let fieldsToReset; if (aFields) { + fieldsToReset = []; for (const fieldName of aFields) { if (!fieldName) { continue; } - const field = this.getField(fieldName); + if (typeof fieldName !== "string") { + // In Acrobat if a fieldName is not a string all the fields are reset. + fieldsToReset = null; + break; + } + const field = this._getField(fieldName); if (!field) { continue; } - field.value = field.defaultValue; - field.valueAsString = field.value; + fieldsToReset.push(field); mustCalculate = true; } - } else { + } + + if (!fieldsToReset) { + fieldsToReset = this._fields.values(); mustCalculate = this._fields.size !== 0; - for (const field of this._fields.values()) { - field.value = field.defaultValue; - field.valueAsString = field.value; - } } + + for (const field of fieldsToReset) { + field.obj.value = field.obj.defaultValue; + this._send({ + id: field.obj._id, + value: field.obj.defaultValue, + formattedValue: null, + selRange: [0, 0], + }); + } + if (mustCalculate) { this.calculateNow(); } diff --git a/src/scripting_api/event.js b/src/scripting_api/event.js index 71591b6f3c067..c57f3ddae15a5 100644 --- a/src/scripting_api/event.js +++ b/src/scripting_api/event.js @@ -45,6 +45,7 @@ class EventDispatcher { this._objects = objects; this._document.obj._eventDispatcher = this; + this._isCalculating = false; } mergeChange(event) { @@ -129,61 +130,84 @@ class EventDispatcher { return; case "Action": this.runActions(source, source, event, name); - if (this._document.obj.calculate) { - this.runCalculate(source, event); - } + this.runCalculate(source, event); return; } this.runActions(source, source, event, name); - if (name === "Keystroke") { - if (event.rc) { - if (event.willCommit) { - this.runValidation(source, event); - } else if ( - event.change !== savedChange.change || + if (name !== "Keystroke") { + return; + } + + if (event.rc) { + if (event.willCommit) { + this.runValidation(source, event); + } else { + const value = (source.obj.value = this.mergeChange(event)); + let selStart, selEnd; + if ( event.selStart !== savedChange.selStart || event.selEnd !== savedChange.selEnd ) { - source.wrapped.value = this.mergeChange(event); + // Selection has been changed by the script so apply the changes. + selStart = event.selStart; + selEnd = event.selEnd; + } else { + selEnd = selStart = savedChange.selStart + event.change.length; } - } else if (!event.willCommit) { source.obj._send({ id: source.obj._id, - value: savedChange.value, - selRange: [savedChange.selStart, savedChange.selEnd], - }); - } else { - // Entry is not valid (rc == false) and it's a commit - // so just clear the field. - source.obj._send({ - id: source.obj._id, - value: "", - selRange: [0, 0], + value, + selRange: [selStart, selEnd], }); } + } else if (!event.willCommit) { + source.obj._send({ + id: source.obj._id, + value: savedChange.value, + selRange: [savedChange.selStart, savedChange.selEnd], + }); + } else { + // Entry is not valid (rc == false) and it's a commit + // so just clear the field. + source.obj._send({ + id: source.obj._id, + value: "", + formattedValue: null, + selRange: [0, 0], + }); } } runValidation(source, event) { - const hasRan = this.runActions(source, source, event, "Validate"); + const didValidateRun = this.runActions(source, source, event, "Validate"); 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; - } + source.obj.value = event.value; - if (this._document.obj.calculate) { - this.runCalculate(source, event); + this.runCalculate(source, event); + + const savedValue = (event.value = source.obj.value); + let formattedValue = null; + + if (this.runActions(source, source, event, "Format")) { + formattedValue = event.value; } - event.value = source.obj.value; - this.runActions(source, source, event, "Format"); - source.wrapped.valueAsString = event.value; + source.obj._send({ + id: source.obj._id, + value: savedValue, + formattedValue, + }); + event.value = savedValue; + } else if (didValidateRun) { + // The value is not valid. + source.obj._send({ + id: source.obj._id, + value: "", + formattedValue: null, + selRange: [0, 0], + }); } } @@ -198,17 +222,42 @@ class EventDispatcher { } calculateNow() { - if (!this._calculationOrder) { + // This function can be called by a JS script (doc.calculateNow()). + // If !this._calculationOrder then there is nothing to calculate. + // _isCalculating is here to prevent infinite recursion with calculateNow. + // If !this._document.obj.calculate then the script doesn't want to have + // a calculate. + + if ( + !this._calculationOrder || + this._isCalculating || + !this._document.obj.calculate + ) { return; } + this._isCalculating = true; const first = this._calculationOrder[0]; const source = this._objects[first]; globalThis.event = new Event({}); - this.runCalculate(source, globalThis.event); + + try { + this.runCalculate(source, globalThis.event); + } catch (error) { + this._isCalculating = false; + throw error; + } + + this._isCalculating = false; } runCalculate(source, event) { - if (!this._calculationOrder) { + // _document.obj.calculate is equivalent to doc.calculate and can be + // changed by a script to allow a future calculate or not. + // This function is either called by calculateNow or when an action + // is triggered (in this case we cannot be currently calculating). + // So there are no need to check for _isCalculating because it has + // been already done in calculateNow. + if (!this._calculationOrder || !this._document.obj.calculate) { return; } @@ -218,31 +267,43 @@ class EventDispatcher { } if (!this._document.obj.calculate) { - // An action may have changed calculate value. - continue; + // An action could have changed calculate value. + break; } event.value = null; const target = this._objects[targetId]; + let savedValue = target.obj.value; this.runActions(source, target, event, "Calculate"); if (!event.rc) { continue; } + if (event.value !== null) { - target.wrapped.value = event.value; + // A new value has been calculated so set it. + target.obj.value = event.value; } event.value = target.obj.value; this.runActions(target, target, event, "Validate"); if (!event.rc) { + if (target.obj.value !== savedValue) { + target.wrapped.value = savedValue; + } continue; } - event.value = target.obj.value; - this.runActions(target, target, event, "Format"); - if (event.value !== null) { - target.wrapped.valueAsString = event.value; + savedValue = event.value = target.obj.value; + let formattedValue = null; + if (this.runActions(target, target, event, "Format")) { + formattedValue = event.value; } + + target.obj._send({ + id: target.obj._id, + value: savedValue, + formattedValue, + }); } } } diff --git a/src/scripting_api/field.js b/src/scripting_api/field.js index 1351054adac85..3c9e957cdec85 100644 --- a/src/scripting_api/field.js +++ b/src/scripting_api/field.js @@ -87,8 +87,6 @@ class Field extends PDFObject { this._globalEval = data.globalEval; this._appObjects = data.appObjects; - - this.valueAsString = data.valueAsString || this._value; } get currentValueIndices() { @@ -252,14 +250,11 @@ class Field extends PDFObject { } get valueAsString() { - if (this._valueAsString === undefined) { - this._valueAsString = this._value ? this._value.toString() : ""; - } - return this._valueAsString; + return (this._value ?? "").toString(); } - set valueAsString(val) { - this._valueAsString = val ? val.toString() : ""; + set valueAsString(_) { + // Do nothing. } browseForFileToSubmit() { @@ -376,7 +371,9 @@ class Field extends PDFObject { } if (this._children === null) { - this._children = this._document.obj._getChildren(this._fieldPath); + this._children = this._document.obj + ._getChildren(this._fieldPath) + .map(child => child.wrapped); } return this._children; } @@ -481,7 +478,7 @@ class Field extends PDFObject { } _reset() { - this.value = this.valueAsString = this.defaultValue; + this.value = this.defaultValue; } _runActions(event) { diff --git a/src/scripting_api/initialization.js b/src/scripting_api/initialization.js index ad33b86a3fc42..7355d58c5a281 100644 --- a/src/scripting_api/initialization.js +++ b/src/scripting_api/initialization.js @@ -120,8 +120,8 @@ function initSandbox(params) { } const wrapped = new Proxy(field, proxyHandler); - doc._addField(name, wrapped); const _object = { obj: field, wrapped }; + doc._addField(name, _object); for (const object of objs) { appObjects[object.id] = _object; } diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index e7cdb9b62da91..a0a8b9a066de7 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -237,7 +237,7 @@ describe("Interaction", () => { await page.click("[data-annotation-id='402R']"); await Promise.all( - ["16", "22", "19", "05", "27"].map(id => + ["16", "22", "19", "05"].map(id => page.waitForFunction( `document.querySelector("#\\\\34 ${id}R").value === ""` ) @@ -256,11 +256,14 @@ describe("Interaction", () => { text = await page.$eval("#\\34 05R", el => el.value); expect(text).toEqual(""); - const sum = await page.$eval("#\\34 27R", el => el.value); - expect(sum).toEqual(""); - checked = await page.$eval("#\\34 49R", el => el.checked); expect(checked).toEqual(false); + + const visibility = await page.$eval( + "#\\34 27R", + el => getComputedStyle(el).visibility + ); + expect(visibility).toEqual("hidden"); }) ); }); @@ -1137,4 +1140,166 @@ describe("Interaction", () => { ); }); }); + + describe("in issue14862.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue14862.pdf", "#\\32 7R"); + pages.map(async ([, page]) => { + page.on("dialog", async dialog => { + await dialog.dismiss(); + }); + }); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must convert input in uppercase", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 7R", "Hello", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "Hello"` + ); + + let text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO"); + + await page.type("#\\32 7R", " world", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO world"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORLD"); + + await page.keyboard.press("Backspace"); + await page.keyboard.press("Backspace"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WORLD"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WOR"); + + await page.type("#\\32 7R", "12.dL", { delay: 100 }); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WOR"` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO WORDL"); + + await page.type("#\\32 7R", " ", { delay: 100 }); + + await page.keyboard.down("Control"); + await page.keyboard.press("Backspace"); + await page.keyboard.up("Control"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 7R").value !== "HELLO WORDL "` + ); + + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HELLO "); + + await page.$eval("#\\32 7R", el => { + // Select LL + el.selectionStart = 2; + el.selectionEnd = 4; + }); + + await page.keyboard.press("a"); + text = await page.$eval("#\\32 7R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("HEAO "); + }) + ); + }); + + it("must check that an infinite loop is not triggered", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 8R", "Hello", { delay: 100 }); + await page.waitForFunction( + `document.querySelector("#\\\\32 8R").value !== "123"` + ); + + let text = await page.$eval("#\\32 8R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("Hello123"); + + // The action will trigger a calculateNow which itself + // will trigger a resetForm (inducing a calculateNow) and a + // calculateNow. + await page.click("[data-annotation-id='31R']"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 8R").value !== "Hello123"` + ); + + // Without preventing against infinite loop the field is empty. + text = await page.$eval("#\\32 8R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("123"); + }) + ); + }); + }); + + describe("in issue14705.pdf", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("issue14705.pdf", "#\\32 9R"); + pages.map(async ([, page]) => { + page.on("dialog", async dialog => { + await dialog.dismiss(); + }); + }); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check that field value is correctly updated", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForFunction( + "window.PDFViewerApplication.scriptingReady === true" + ); + + await page.type("#\\32 9R", "Hello World", { delay: 100 }); + await page.click("#\\32 7R"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 9R").value !== "Hello World"` + ); + + let text = await page.$eval("#\\32 9R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("checked"); + + await page.click("#\\32 7R"); + + await page.waitForFunction( + `document.querySelector("#\\\\32 9R").value !== "checked"` + ); + + text = await page.$eval("#\\32 9R", el => el.value); + expect(text).withContext(`In ${browserName}`).toEqual("unchecked"); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index c8511d9c65b1c..2dc4fcd79d082 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -520,3 +520,5 @@ !issue14502.pdf !issue13211.pdf !issue14627.pdf +!issue14862.pdf +!issue14705.pdf diff --git a/test/pdfs/issue14705.pdf b/test/pdfs/issue14705.pdf new file mode 100644 index 0000000000000000000000000000000000000000..09220a70eaa53f4d070617e6b1ab4986782cc550 GIT binary patch literal 7755 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 LiK5)TL&$#ve8Rb+ literal 0 HcmV?d00001 diff --git a/test/pdfs/issue14862.pdf b/test/pdfs/issue14862.pdf new file mode 100755 index 0000000000000000000000000000000000000000..be745fab0e3c5b7304a2c6465c84db249fdc6603 GIT binary patch literal 13634 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 literal 0 HcmV?d00001 diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 4cf8c7610ea09..ed7b3212afa9e 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -1320,7 +1320,7 @@ describe("api", function () { { id: "25R", value: "", - defaultValue: null, + defaultValue: "", multiline: false, password: false, charLimit: null, diff --git a/test/unit/scripting_spec.js b/test/unit/scripting_spec.js index 72af2150b5ebf..0b1763b9e65b7 100644 --- a/test/unit/scripting_spec.js +++ b/test/unit/scripting_spec.js @@ -89,7 +89,7 @@ describe("Scripting", function () { return s; } const number = 123; - const expected = (((number - 1) * number) / 2).toString(); + const expected = ((number - 1) * number) / 2; const refId = getId(); const data = { @@ -120,7 +120,8 @@ describe("Scripting", function () { expect(send_queue.has(refId)).toEqual(true); expect(send_queue.get(refId)).toEqual({ id: refId, - valueAsString: expected, + value: expected, + formattedValue: null, }); }); }); @@ -406,6 +407,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "hella", + selRange: [5, 5], }); }); @@ -478,7 +480,7 @@ describe("Scripting", function () { expect(send_queue.get(refId1)).toEqual({ id: refId1, value: "world", - valueAsString: "world", + formattedValue: null, }); }); }); @@ -799,7 +801,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "123456.789", - valueAsString: "123456.789", + formattedValue: null, }); }); @@ -978,7 +980,7 @@ describe("Scripting", function () { expect(send_queue.get(refId)).toEqual({ id: refId, value: "321", - valueAsString: "321", + formattedValue: null, }); }); @@ -1076,7 +1078,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 1, - valueAsString: "1", + formattedValue: null, }); await sandbox.dispatchEventInSandbox({ @@ -1089,7 +1091,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 3, - valueAsString: "3", + formattedValue: null, }); await sandbox.dispatchEventInSandbox({ @@ -1102,7 +1104,7 @@ describe("Scripting", function () { expect(send_queue.get(refIds[3])).toEqual({ id: refIds[3], value: 6, - valueAsString: "6", + formattedValue: null, }); }); }); @@ -1137,7 +1139,8 @@ describe("Scripting", function () { selStart: 0, selEnd: 0, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1148,7 +1151,8 @@ describe("Scripting", function () { selStart: 1, selEnd: 1, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1159,7 +1163,8 @@ describe("Scripting", function () { selStart: 2, selEnd: 2, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1187,7 +1192,8 @@ describe("Scripting", function () { selStart: 3, selEnd: 3, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); await sandbox.dispatchEventInSandbox({ id: refId, @@ -1200,7 +1206,8 @@ describe("Scripting", function () { expect(send_queue.has(refId)).toEqual(true); expect(send_queue.get(refId)).toEqual({ id: refId, - valueAsString: "3F?0", + value: "3F?0", + formattedValue: null, }); }); }); @@ -1242,7 +1249,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; } @@ -1301,7 +1309,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; } @@ -1360,7 +1369,8 @@ describe("Scripting", function () { selStart: i, selEnd: i, }); - expect(send_queue.has(refId)).toEqual(false); + expect(send_queue.has(refId)).toEqual(true); + send_queue.delete(refId); value += change; } From 6b866a5efaab64319e78e3f9a0d65b5b7b03b57f Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Tue, 3 May 2022 16:25:29 +0200 Subject: [PATCH 2/2] Add a small delay when typing in some integration tests to avoid intermittent failures --- test/integration/scripting_spec.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/integration/scripting_spec.js b/test/integration/scripting_spec.js index a0a8b9a066de7..d7f7f65b2a3bc 100644 --- a/test/integration/scripting_spec.js +++ b/test/integration/scripting_spec.js @@ -995,7 +995,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "12A"); + await page.type("#\\32 9R", "12A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\32 9R").value !== "12A"` ); @@ -1004,7 +1004,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("12"); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "34"); + await page.type("#\\32 9R", "34", { delay: 100 }); await page.click("[data-annotation-id='30R']"); await page.waitForFunction( @@ -1015,7 +1015,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\32 9R"); - await page.type("#\\32 9R", "12345"); + await page.type("#\\32 9R", "12345", { delay: 100 }); await page.click("[data-annotation-id='30R']"); text = await page.$eval(`#\\32 9R`, el => el.value); @@ -1052,7 +1052,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "(123) 456A"); + await page.type("#\\33 0R", "(123) 456A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\33 0R").value !== "(123) 456A"` ); @@ -1061,7 +1061,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("(123) 456"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "-789"); + await page.type("#\\33 0R", "-789", { delay: 100 }); await page.click("[data-annotation-id='29R']"); await page.waitForFunction( @@ -1072,7 +1072,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "(123) 456-7890"); + await page.type("#\\33 0R", "(123) 456-7890", { delay: 100 }); await page.click("[data-annotation-id='29R']"); text = await page.$eval(`#\\33 0R`, el => el.value); @@ -1111,7 +1111,7 @@ describe("Interaction", () => { await clearInput(page, "#\\33 0R"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "123A"); + await page.type("#\\33 0R", "123A", { delay: 100 }); await page.waitForFunction( `document.querySelector("#\\\\33 0R").value !== "123A"` ); @@ -1120,7 +1120,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual("123"); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "-456"); + await page.type("#\\33 0R", "-456", { delay: 100 }); await page.click("[data-annotation-id='29R']"); await page.waitForFunction( @@ -1131,7 +1131,7 @@ describe("Interaction", () => { expect(text).withContext(`In ${browserName}`).toEqual(""); await page.focus("#\\33 0R"); - await page.type("#\\33 0R", "123-4567"); + await page.type("#\\33 0R", "123-4567", { delay: 100 }); await page.click("[data-annotation-id='29R']"); text = await page.$eval(`#\\33 0R`, el => el.value);